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.util.Log
21 import androidx.annotation.FloatRange
22 import androidx.annotation.VisibleForTesting
23 import androidx.annotation.WorkerThread
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.updates.hinge.FULLY_CLOSED_DEGREES
28 import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
29 import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
30 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
31 import com.android.systemui.unfold.util.CurrentActivityTypeProvider
32 import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityProvider
33 import dagger.assisted.Assisted
34 import dagger.assisted.AssistedFactory
35 import dagger.assisted.AssistedInject
36 import java.util.concurrent.CopyOnWriteArrayList
37 import java.util.concurrent.Executor
38 
39 class DeviceFoldStateProvider
40 @AssistedInject
41 constructor(
42     config: UnfoldTransitionConfig,
43     private val context: Context,
44     private val screenStatusProvider: ScreenStatusProvider,
45     private val activityTypeProvider: CurrentActivityTypeProvider,
46     private val unfoldKeyguardVisibilityProvider: UnfoldKeyguardVisibilityProvider,
47     private val foldProvider: FoldProvider,
48     @Assisted private val hingeAngleProvider: HingeAngleProvider,
49     @Assisted private val rotationChangeProvider: RotationChangeProvider,
50     @Assisted private val progressHandler: Handler,
51 ) : FoldStateProvider {
52     private val outputListeners = CopyOnWriteArrayList<FoldStateProvider.FoldUpdatesListener>()
53 
54     @FoldStateProvider.FoldUpdate private var lastFoldUpdate: Int? = null
55 
56     @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
57     @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngleBeforeTransition: Float = 0f
58 
59     private val hingeAngleListener = HingeAngleListener()
60     private val screenListener = ScreenStatusListener()
61     private val foldStateListener = FoldStateListener()
<lambda>null62     private val timeoutRunnable = Runnable { cancelAnimation() }
63     private val rotationListener = FoldRotationListener()
<lambda>null64     private val progressExecutor = Executor { progressHandler.post(it) }
65 
66     /**
67      * Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
68      * [FOLD_UPDATE_START_CLOSING] or [FOLD_UPDATE_START_OPENING] event, if an end state is not
69      * reached.
70      */
71     private val halfOpenedTimeoutMillis: Int = config.halfFoldedTimeoutMillis
72 
73     private var isFolded = false
74     private var isScreenOn = false
75     private var isUnfoldHandled = true
76     private var isStarted = false
77 
startnull78     override fun start() {
79         if (isStarted) return
80         foldProvider.registerCallback(foldStateListener, progressExecutor)
81         // TODO(b/277879146): get callbacks in the background
82         screenStatusProvider.addCallback(screenListener)
83         hingeAngleProvider.addCallback(hingeAngleListener)
84         rotationChangeProvider.addCallback(rotationListener)
85         activityTypeProvider.init()
86         isStarted = true
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         isStarted = false
97     }
98 
addCallbacknull99     override fun addCallback(listener: FoldStateProvider.FoldUpdatesListener) {
100         outputListeners.add(listener)
101     }
102 
removeCallbacknull103     override fun removeCallback(listener: FoldStateProvider.FoldUpdatesListener) {
104         outputListeners.remove(listener)
105     }
106 
107     override val isFinishedOpening: Boolean
108         get() =
109             !isFolded &&
110                 (lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
111                     lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
112 
113     private val isTransitionInProgress: Boolean
114         get() =
115             lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
116                 lastFoldUpdate == FOLD_UPDATE_START_CLOSING
117 
onHingeAnglenull118     private fun onHingeAngle(angle: Float) {
119         assertInProgressThread()
120         if (DEBUG) {
121             Log.d(
122                 TAG,
123                 "Hinge angle: $angle, " +
124                     "lastHingeAngle: $lastHingeAngle, " +
125                     "lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition"
126             )
127         }
128 
129         val currentDirection =
130             if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
131         val changedDirectionWhileInTransition =
132             isTransitionInProgress && currentDirection != lastFoldUpdate
133         val unfoldedPastThresholdSinceLastTransition =
134             angle - lastHingeAngleBeforeTransition > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
135         if (changedDirectionWhileInTransition || unfoldedPastThresholdSinceLastTransition) {
136             lastHingeAngleBeforeTransition = lastHingeAngle
137         }
138 
139         val isClosing = angle < lastHingeAngleBeforeTransition
140         val transitionUpdate =
141             if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
142         val angleChangeSurpassedThreshold =
143             Math.abs(angle - lastHingeAngleBeforeTransition) > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
144         val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
145         val eventNotAlreadyDispatched = lastFoldUpdate != transitionUpdate
146         val screenAvailableEventSent = isUnfoldHandled
147         val isOnLargeScreen = isOnLargeScreen()
148 
149         if (
150             angleChangeSurpassedThreshold && // Do not react immediately to small changes in angle
151                 eventNotAlreadyDispatched && // we haven't sent transition event already
152                 !isFullyOpened && // do not send transition event if we are in fully opened hinge
153                 // angle range as closing threshold could overlap this range
154                 screenAvailableEventSent && // do not send transition event if we are still in the
155                 // process of turning on the inner display
156                 isClosingThresholdMet(angle) && // hinge angle is below certain threshold.
157                 isOnLargeScreen // Avoids sending closing event when on small screen.
158         // Start event is sent regardless due to hall sensor.
159         ) {
160             notifyFoldUpdate(transitionUpdate, angle)
161         }
162 
163         if (isTransitionInProgress) {
164             if (isFullyOpened) {
165                 notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN, angle)
166                 cancelTimeout()
167             } else {
168                 // The timeout will trigger some constant time after the last angle update.
169                 rescheduleAbortAnimationTimeout()
170             }
171         }
172 
173         lastHingeAngle = angle
174         outputListeners.forEach { it.onHingeAngleUpdate(angle) }
175     }
176 
isClosingThresholdMetnull177     private fun isClosingThresholdMet(currentAngle: Float): Boolean {
178         val closingThreshold = getClosingThreshold()
179         return closingThreshold == null || currentAngle < closingThreshold
180     }
181 
182     /**
183      * Fold animation should be started only after the threshold returned here.
184      *
185      * This has been introduced because the fold animation might be distracting/unwanted on top of
186      * apps that support table-top/HALF_FOLDED mode. Only for launcher, there is no threshold.
187      */
getClosingThresholdnull188     private fun getClosingThreshold(): Int? {
189         val isHomeActivity = activityTypeProvider.isHomeActivity ?: return null
190         val isKeyguardVisible = unfoldKeyguardVisibilityProvider.isKeyguardVisible == true
191 
192         if (DEBUG) {
193             Log.d(TAG, "isHomeActivity=$isHomeActivity, isOnKeyguard=$isKeyguardVisible")
194         }
195 
196         return if (isHomeActivity || isKeyguardVisible) {
197             null
198         } else {
199             START_CLOSING_ON_APPS_THRESHOLD_DEGREES
200         }
201     }
202 
203     private inner class FoldStateListener : FoldProvider.FoldCallback {
onFoldUpdatednull204         override fun onFoldUpdated(isFolded: Boolean) {
205             assertInProgressThread()
206             this@DeviceFoldStateProvider.isFolded = isFolded
207             lastHingeAngle = FULLY_CLOSED_DEGREES
208 
209             if (isFolded) {
210                 hingeAngleProvider.stop()
211                 notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED, lastHingeAngle)
212                 cancelTimeout()
213                 isUnfoldHandled = false
214             } else {
215                 notifyFoldUpdate(FOLD_UPDATE_START_OPENING, lastHingeAngle)
216                 rescheduleAbortAnimationTimeout()
217                 hingeAngleProvider.start()
218             }
219         }
220     }
221 
222     private inner class FoldRotationListener : RotationChangeProvider.RotationListener {
223         @WorkerThread
onRotationChangednull224         override fun onRotationChanged(newRotation: Int) {
225             assertInProgressThread()
226             if (isTransitionInProgress) cancelAnimation()
227         }
228     }
229 
notifyFoldUpdatenull230     private fun notifyFoldUpdate(@FoldStateProvider.FoldUpdate update: Int, angle: Float) {
231         if (DEBUG) {
232             Log.d(TAG, update.name())
233         }
234         val previouslyTransitioning = isTransitionInProgress
235 
236         outputListeners.forEach { it.onFoldUpdate(update) }
237         lastFoldUpdate = update
238 
239         if (previouslyTransitioning != isTransitionInProgress) {
240             lastHingeAngleBeforeTransition = angle
241         }
242     }
243 
rescheduleAbortAnimationTimeoutnull244     private fun rescheduleAbortAnimationTimeout() {
245         if (isTransitionInProgress) {
246             cancelTimeout()
247         }
248         progressHandler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong())
249     }
250 
cancelTimeoutnull251     private fun cancelTimeout() {
252         progressHandler.removeCallbacks(timeoutRunnable)
253     }
254 
cancelAnimationnull255     private fun cancelAnimation(): Unit =
256         notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN, lastHingeAngle)
257 
258     private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
259 
260         override fun onScreenTurnedOn() {
261             executeInProgressThread {
262                 // Trigger this event only if we are unfolded and this is the first screen
263                 // turned on event since unfold started. This prevents running the animation when
264                 // turning on the internal display using the power button.
265                 // Initially isUnfoldHandled is true so it will be reset to false *only* when we
266                 // receive 'folded' event. If SystemUI started when device is already folded it will
267                 // still receive 'folded' event on startup.
268                 if (!isFolded && !isUnfoldHandled) {
269                     outputListeners.forEach { it.onUnfoldedScreenAvailable() }
270                     isUnfoldHandled = true
271                 }
272             }
273         }
274 
275         override fun markScreenAsTurnedOn() {
276             executeInProgressThread {
277                 if (!isFolded) {
278                     isUnfoldHandled = true
279                 }
280             }
281         }
282 
283         override fun onScreenTurningOn() {
284             executeInProgressThread {
285                 isScreenOn = true
286                 updateHingeAngleProviderState()
287             }
288         }
289 
290         override fun onScreenTurningOff() {
291             executeInProgressThread {
292                 isScreenOn = false
293                 updateHingeAngleProviderState()
294             }
295         }
296 
297         /**
298          * Needed just for compatibility while not all data sources are providing data in the
299          * background.
300          *
301          * TODO(b/277879146): Remove once ScreeStatusProvider provides in the background.
302          */
303         private fun executeInProgressThread(f: () -> Unit) {
304             progressHandler.post { f() }
305         }
306     }
307 
isOnLargeScreennull308     private fun isOnLargeScreen(): Boolean {
309         return context.applicationContext.resources.configuration.smallestScreenWidthDp >
310             INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
311     }
312 
313     /** While the screen is off or the device is folded, hinge angle updates are not needed. */
updateHingeAngleProviderStatenull314     private fun updateHingeAngleProviderState() {
315         assertInProgressThread()
316         if (isScreenOn && !isFolded) {
317             hingeAngleProvider.start()
318         } else {
319             hingeAngleProvider.stop()
320         }
321     }
322 
323     private inner class HingeAngleListener : Consumer<Float> {
acceptnull324         override fun accept(angle: Float) {
325             assertInProgressThread()
326             onHingeAngle(angle)
327         }
328     }
329 
assertInProgressThreadnull330     private fun assertInProgressThread() {
331         check(progressHandler.looper.isCurrentThread) {
332             val progressThread = progressHandler.looper.thread
333             val thisThread = Thread.currentThread()
334             """should be called from the progress thread.
335                 progressThread=$progressThread tid=${progressThread.id}
336                 Thread.currentThread()=$thisThread tid=${thisThread.id}"""
337                 .trimMargin()
338         }
339     }
340 
341     @AssistedFactory
342     interface Factory {
343         /** Creates a [DeviceFoldStateProvider] using the provided dependencies. */
createnull344         fun create(
345             hingeAngleProvider: HingeAngleProvider,
346             rotationChangeProvider: RotationChangeProvider,
347             progressHandler: Handler,
348         ): DeviceFoldStateProvider
349     }
350 }
351 
352 fun @receiver:FoldStateProvider.FoldUpdate Int.name() =
353     when (this) {
354         FOLD_UPDATE_START_OPENING -> "START_OPENING"
355         FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
356         FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN"
357         FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN"
358         FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
359         else -> "UNKNOWN"
360     }
361 
362 private const val TAG = "DeviceFoldProvider"
363 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
364 
365 /** Threshold after which we consider the device fully unfolded. */
366 @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
367 
368 /** Threshold after which hinge angle updates are considered. This is to eliminate noise. */
369 @VisibleForTesting const val HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES = 7.5f
370 
371 /** Fold animation on top of apps only when the angle exceeds this threshold. */
372 @VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
373