• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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