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