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.annotation.FloatRange
19 import android.content.Context
20 import android.hardware.devicestate.DeviceStateManager
21 import android.os.Handler
22 import android.util.Log
23 import androidx.annotation.VisibleForTesting
24 import androidx.core.util.Consumer
25 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
26 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
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 java.util.concurrent.Executor
32 
33 class DeviceFoldStateProvider(
34     context: Context,
35     private val hingeAngleProvider: HingeAngleProvider,
36     private val screenStatusProvider: ScreenStatusProvider,
37     private val deviceStateManager: DeviceStateManager,
38     private val mainExecutor: Executor,
39     private val handler: Handler
40 ) : FoldStateProvider {
41 
42     private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
43 
44     @FoldUpdate
45     private var lastFoldUpdate: Int? = null
46 
47     @FloatRange(from = 0.0, to = 180.0)
48     private var lastHingeAngle: Float = 0f
49 
50     private val hingeAngleListener = HingeAngleListener()
51     private val screenListener = ScreenStatusListener()
52     private val foldStateListener = FoldStateListener(context)
53     private val timeoutRunnable = TimeoutRunnable()
54 
55     private var isFolded = false
56     private var isUnfoldHandled = true
57 
58     override fun start() {
59         deviceStateManager.registerCallback(
60             mainExecutor,
61             foldStateListener
62         )
63         screenStatusProvider.addCallback(screenListener)
64         hingeAngleProvider.addCallback(hingeAngleListener)
65     }
66 
67     override fun stop() {
68         screenStatusProvider.removeCallback(screenListener)
69         deviceStateManager.unregisterCallback(foldStateListener)
70         hingeAngleProvider.removeCallback(hingeAngleListener)
71         hingeAngleProvider.stop()
72     }
73 
74     override fun addCallback(listener: FoldUpdatesListener) {
75         outputListeners.add(listener)
76     }
77 
78     override fun removeCallback(listener: FoldUpdatesListener) {
79         outputListeners.remove(listener)
80     }
81 
82     override val isFullyOpened: Boolean
83         get() = !isFolded && lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN
84 
85     private val isTransitionInProgess: Boolean
86         get() = lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
87                 lastFoldUpdate == FOLD_UPDATE_START_CLOSING
88 
89     private fun onHingeAngle(angle: Float) {
90         if (DEBUG) { Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle") }
91 
92         val isClosing = angle < lastHingeAngle
93         val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
94         val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
95 
96         if (isClosing && !closingEventDispatched && !isFullyOpened) {
97             notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
98         }
99 
100         if (isTransitionInProgess) {
101             if (isFullyOpened) {
102                 notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
103                 cancelTimeout()
104             } else {
105                 // The timeout will trigger some constant time after the last angle update.
106                 rescheduleAbortAnimationTimeout()
107             }
108         }
109 
110         lastHingeAngle = angle
111         outputListeners.forEach { it.onHingeAngleUpdate(angle) }
112     }
113 
114     private inner class FoldStateListener(context: Context) :
115         DeviceStateManager.FoldStateListener(context, { folded: Boolean ->
116             isFolded = folded
117             lastHingeAngle = FULLY_CLOSED_DEGREES
118 
119             if (folded) {
120                 hingeAngleProvider.stop()
121                 notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
122                 cancelTimeout()
123                 isUnfoldHandled = false
124             } else {
125                 notifyFoldUpdate(FOLD_UPDATE_START_OPENING)
126                 rescheduleAbortAnimationTimeout()
127                 hingeAngleProvider.start()
128             }
129         })
130 
131     private fun notifyFoldUpdate(@FoldUpdate update: Int) {
132         if (DEBUG) { Log.d(TAG, stateToString(update)) }
133         outputListeners.forEach { it.onFoldUpdate(update) }
134         lastFoldUpdate = update
135     }
136 
137     private fun rescheduleAbortAnimationTimeout() {
138         if (isTransitionInProgess) {
139             cancelTimeout()
140         }
141         handler.postDelayed(timeoutRunnable, ABORT_CLOSING_MILLIS)
142     }
143 
144     private fun cancelTimeout() {
145         handler.removeCallbacks(timeoutRunnable)
146     }
147 
148     private inner class ScreenStatusListener :
149         ScreenStatusProvider.ScreenListener {
150 
151         override fun onScreenTurnedOn() {
152             // Trigger this event only if we are unfolded and this is the first screen
153             // turned on event since unfold started. This prevents running the animation when
154             // turning on the internal display using the power button.
155             // Initially isUnfoldHandled is true so it will be reset to false *only* when we
156             // receive 'folded' event. If SystemUI started when device is already folded it will
157             // still receive 'folded' event on startup.
158             if (!isFolded && !isUnfoldHandled) {
159                 outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) }
160                 isUnfoldHandled = true
161             }
162         }
163     }
164 
165     private inner class HingeAngleListener : Consumer<Float> {
166 
167         override fun accept(angle: Float) {
168             onHingeAngle(angle)
169         }
170     }
171 
172     private inner class TimeoutRunnable : Runnable {
173 
174         override fun run() {
175             notifyFoldUpdate(FOLD_UPDATE_ABORTED)
176         }
177     }
178 }
179 
180 private fun stateToString(@FoldUpdate update: Int): String {
181     return when (update) {
182         FOLD_UPDATE_START_OPENING -> "START_OPENING"
183         FOLD_UPDATE_HALF_OPEN -> "HALF_OPEN"
184         FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
185         FOLD_UPDATE_ABORTED -> "ABORTED"
186         FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE"
187         FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN"
188         FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN"
189         FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
190         else -> "UNKNOWN"
191     }
192 }
193 
194 private const val TAG = "DeviceFoldProvider"
195 private const val DEBUG = false
196 
197 /**
198  * Time after which [FOLD_UPDATE_ABORTED] is emitted following a [FOLD_UPDATE_START_CLOSING] or
199  * [FOLD_UPDATE_START_OPENING] event, if an end state is not reached.
200  */
201 @VisibleForTesting
202 const val ABORT_CLOSING_MILLIS = 1000L
203 
204 /** Threshold after which we consider the device fully unfolded. */
205 @VisibleForTesting
206 const val FULLY_OPEN_THRESHOLD_DEGREES = 15f