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()
65 private val mainLooper = handler.looper
<lambda>null66 private val timeoutRunnable = Runnable { cancelAnimation() }
<lambda>null67 private val rotationListener = RotationListener {
68 if (isTransitionInProgress) cancelAnimation()
69 }
70
71 /**
72 * Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
73 * [FOLD_UPDATE_START_CLOSING] or [FOLD_UPDATE_START_OPENING] event, if an end state is not
74 * reached.
75 */
76 private val halfOpenedTimeoutMillis: Int = config.halfFoldedTimeoutMillis
77
78 private var isFolded = false
79 private var isScreenOn = false
80 private var isUnfoldHandled = true
81 private var isStarted = false
82
startnull83 override fun start() {
84 assertMainThread()
85 if (isStarted) return
86 foldProvider.registerCallback(foldStateListener, mainExecutor)
87 screenStatusProvider.addCallback(screenListener)
88 hingeAngleProvider.addCallback(hingeAngleListener)
89 rotationChangeProvider.addCallback(rotationListener)
90 activityTypeProvider.init()
91 isStarted = true
92 }
93
stopnull94 override fun stop() {
95 assertMainThread()
96 screenStatusProvider.removeCallback(screenListener)
97 foldProvider.unregisterCallback(foldStateListener)
98 hingeAngleProvider.removeCallback(hingeAngleListener)
99 hingeAngleProvider.stop()
100 rotationChangeProvider.removeCallback(rotationListener)
101 activityTypeProvider.uninit()
102 isStarted = false
103 }
104
addCallbacknull105 override fun addCallback(listener: FoldUpdatesListener) {
106 outputListeners.add(listener)
107 }
108
removeCallbacknull109 override fun removeCallback(listener: FoldUpdatesListener) {
110 outputListeners.remove(listener)
111 }
112
113 override val isFinishedOpening: Boolean
114 get() =
115 !isFolded &&
116 (lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
117 lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
118
119 private val isTransitionInProgress: Boolean
120 get() =
121 lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
122 lastFoldUpdate == FOLD_UPDATE_START_CLOSING
123
onHingeAnglenull124 private fun onHingeAngle(angle: Float) {
125 if (DEBUG) {
126 Log.d(
127 TAG,
128 "Hinge angle: $angle, " +
129 "lastHingeAngle: $lastHingeAngle, " +
130 "lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition"
131 )
132 }
133 Trace.setCounter("DeviceFoldStateProvider#onHingeAngle", angle.toLong())
134
135 val currentDirection =
136 if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
137 if (isTransitionInProgress && currentDirection != lastFoldUpdate) {
138 lastHingeAngleBeforeTransition = lastHingeAngle
139 }
140
141 val isClosing = angle < lastHingeAngleBeforeTransition
142 val transitionUpdate =
143 if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
144 val angleChangeSurpassedThreshold =
145 Math.abs(angle - lastHingeAngleBeforeTransition) > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
146 val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
147 val eventNotAlreadyDispatched = lastFoldUpdate != transitionUpdate
148 val screenAvailableEventSent = isUnfoldHandled
149 val isOnLargeScreen = isOnLargeScreen()
150
151 if (
152 angleChangeSurpassedThreshold && // Do not react immediately to small changes in angle
153 eventNotAlreadyDispatched && // we haven't sent transition event already
154 !isFullyOpened && // do not send transition event if we are in fully opened hinge
155 // angle range as closing threshold could overlap this range
156 screenAvailableEventSent && // do not send transition event if we are still in the
157 // process of turning on the inner display
158 isClosingThresholdMet(angle) && // hinge angle is below certain threshold.
159 isOnLargeScreen // Avoids sending closing event when on small screen.
160 // Start event is sent regardless due to hall sensor.
161 ) {
162 notifyFoldUpdate(transitionUpdate, lastHingeAngle)
163 }
164
165 if (isTransitionInProgress) {
166 if (isFullyOpened) {
167 notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN, angle)
168 cancelTimeout()
169 } else {
170 // The timeout will trigger some constant time after the last angle update.
171 rescheduleAbortAnimationTimeout()
172 }
173 }
174
175 lastHingeAngle = angle
176 outputListeners.forEach { it.onHingeAngleUpdate(angle) }
177 }
178
isClosingThresholdMetnull179 private fun isClosingThresholdMet(currentAngle: Float): Boolean {
180 val closingThreshold = getClosingThreshold()
181 return closingThreshold == null || currentAngle < closingThreshold
182 }
183
184 /**
185 * Fold animation should be started only after the threshold returned here.
186 *
187 * This has been introduced because the fold animation might be distracting/unwanted on top of
188 * apps that support table-top/HALF_FOLDED mode. Only for launcher, there is no threshold.
189 */
getClosingThresholdnull190 private fun getClosingThreshold(): Int? {
191 val isHomeActivity = activityTypeProvider.isHomeActivity ?: return null
192 val isKeyguardVisible = unfoldKeyguardVisibilityProvider.isKeyguardVisible == true
193
194 if (DEBUG) {
195 Log.d(TAG, "isHomeActivity=$isHomeActivity, isOnKeyguard=$isKeyguardVisible")
196 }
197
198 return if (isHomeActivity || isKeyguardVisible) {
199 null
200 } else {
201 START_CLOSING_ON_APPS_THRESHOLD_DEGREES
202 }
203 }
204
205 private inner class FoldStateListener : FoldProvider.FoldCallback {
onFoldUpdatednull206 override fun onFoldUpdated(isFolded: Boolean) {
207 this@DeviceFoldStateProvider.isFolded = isFolded
208 lastHingeAngle = FULLY_CLOSED_DEGREES
209
210 if (isFolded) {
211 hingeAngleProvider.stop()
212 notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED, lastHingeAngle)
213 cancelTimeout()
214 isUnfoldHandled = false
215 } else {
216 notifyFoldUpdate(FOLD_UPDATE_START_OPENING, lastHingeAngle)
217 rescheduleAbortAnimationTimeout()
218 hingeAngleProvider.start()
219 }
220 }
221 }
222
notifyFoldUpdatenull223 private fun notifyFoldUpdate(@FoldUpdate update: Int, angle: Float) {
224 if (DEBUG) {
225 Log.d(TAG, update.name())
226 }
227 val previouslyTransitioning = isTransitionInProgress
228
229 outputListeners.forEach { it.onFoldUpdate(update) }
230 lastFoldUpdate = update
231
232 if (previouslyTransitioning != isTransitionInProgress) {
233 lastHingeAngleBeforeTransition = angle
234 }
235 }
236
rescheduleAbortAnimationTimeoutnull237 private fun rescheduleAbortAnimationTimeout() {
238 if (isTransitionInProgress) {
239 cancelTimeout()
240 }
241 handler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong())
242 }
243
cancelTimeoutnull244 private fun cancelTimeout() {
245 handler.removeCallbacks(timeoutRunnable)
246 }
247
cancelAnimationnull248 private fun cancelAnimation(): Unit =
249 notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN, lastHingeAngle)
250
251 private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
252
253 override fun onScreenTurnedOn() {
254 // Trigger this event only if we are unfolded and this is the first screen
255 // turned on event since unfold started. This prevents running the animation when
256 // turning on the internal display using the power button.
257 // Initially isUnfoldHandled is true so it will be reset to false *only* when we
258 // receive 'folded' event. If SystemUI started when device is already folded it will
259 // still receive 'folded' event on startup.
260 if (!isFolded && !isUnfoldHandled) {
261 outputListeners.forEach { it.onUnfoldedScreenAvailable() }
262 isUnfoldHandled = true
263 }
264 }
265
266 override fun markScreenAsTurnedOn() {
267 if (!isFolded) {
268 isUnfoldHandled = true
269 }
270 }
271
272 override fun onScreenTurningOn() {
273 isScreenOn = true
274 updateHingeAngleProviderState()
275 }
276
277 override fun onScreenTurningOff() {
278 isScreenOn = false
279 updateHingeAngleProviderState()
280 }
281 }
282
isOnLargeScreennull283 private fun isOnLargeScreen(): Boolean {
284 return context.resources.configuration.smallestScreenWidthDp >
285 INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
286 }
287
288 /** While the screen is off or the device is folded, hinge angle updates are not needed. */
updateHingeAngleProviderStatenull289 private fun updateHingeAngleProviderState() {
290 if (isScreenOn && !isFolded) {
291 hingeAngleProvider.start()
292 } else {
293 hingeAngleProvider.stop()
294 }
295 }
296
297 private inner class HingeAngleListener : Consumer<Float> {
acceptnull298 override fun accept(angle: Float) {
299 onHingeAngle(angle)
300 }
301 }
302
assertMainThreadnull303 private fun assertMainThread() {
304 check(mainLooper.isCurrentThread) {
305 ("should be called from the main thread." +
306 " sMainLooper.threadName=" + mainLooper.thread.name +
307 " Thread.currentThread()=" + Thread.currentThread().name)
308 }
309 }
310 }
311
namenull312 fun @receiver:FoldUpdate Int.name() =
313 when (this) {
314 FOLD_UPDATE_START_OPENING -> "START_OPENING"
315 FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
316 FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN"
317 FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN"
318 FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
319 else -> "UNKNOWN"
320 }
321
322 private const val TAG = "DeviceFoldProvider"
323 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
324
325 /** Threshold after which we consider the device fully unfolded. */
326 @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
327
328 /** Threshold after which hinge angle updates are considered. This is to eliminate noise. */
329 @VisibleForTesting const val HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES = 7.5f
330
331 /** Fold animation on top of apps only when the angle exceeds this threshold. */
332 @VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
333