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 if (isTransitionInProgress && currentDirection != lastFoldUpdate) { 132 lastHingeAngleBeforeTransition = lastHingeAngle 133 } 134 135 val isClosing = angle < lastHingeAngleBeforeTransition 136 val transitionUpdate = 137 if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING 138 val angleChangeSurpassedThreshold = 139 Math.abs(angle - lastHingeAngleBeforeTransition) > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES 140 val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES 141 val eventNotAlreadyDispatched = lastFoldUpdate != transitionUpdate 142 val screenAvailableEventSent = isUnfoldHandled 143 val isOnLargeScreen = isOnLargeScreen() 144 145 if ( 146 angleChangeSurpassedThreshold && // Do not react immediately to small changes in angle 147 eventNotAlreadyDispatched && // we haven't sent transition event already 148 !isFullyOpened && // do not send transition event if we are in fully opened hinge 149 // angle range as closing threshold could overlap this range 150 screenAvailableEventSent && // do not send transition event if we are still in the 151 // process of turning on the inner display 152 isClosingThresholdMet(angle) && // hinge angle is below certain threshold. 153 isOnLargeScreen // Avoids sending closing event when on small screen. 154 // Start event is sent regardless due to hall sensor. 155 ) { 156 notifyFoldUpdate(transitionUpdate, lastHingeAngle) 157 } 158 159 if (isTransitionInProgress) { 160 if (isFullyOpened) { 161 notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN, angle) 162 cancelTimeout() 163 } else { 164 // The timeout will trigger some constant time after the last angle update. 165 rescheduleAbortAnimationTimeout() 166 } 167 } 168 169 lastHingeAngle = angle 170 outputListeners.forEach { it.onHingeAngleUpdate(angle) } 171 } 172 isClosingThresholdMetnull173 private fun isClosingThresholdMet(currentAngle: Float): Boolean { 174 val closingThreshold = getClosingThreshold() 175 return closingThreshold == null || currentAngle < closingThreshold 176 } 177 178 /** 179 * Fold animation should be started only after the threshold returned here. 180 * 181 * This has been introduced because the fold animation might be distracting/unwanted on top of 182 * apps that support table-top/HALF_FOLDED mode. Only for launcher, there is no threshold. 183 */ getClosingThresholdnull184 private fun getClosingThreshold(): Int? { 185 val isHomeActivity = activityTypeProvider.isHomeActivity ?: return null 186 val isKeyguardVisible = unfoldKeyguardVisibilityProvider.isKeyguardVisible == true 187 188 if (DEBUG) { 189 Log.d(TAG, "isHomeActivity=$isHomeActivity, isOnKeyguard=$isKeyguardVisible") 190 } 191 192 return if (isHomeActivity || isKeyguardVisible) { 193 null 194 } else { 195 START_CLOSING_ON_APPS_THRESHOLD_DEGREES 196 } 197 } 198 199 private inner class FoldStateListener : FoldProvider.FoldCallback { onFoldUpdatednull200 override fun onFoldUpdated(isFolded: Boolean) { 201 assertInProgressThread() 202 this@DeviceFoldStateProvider.isFolded = isFolded 203 lastHingeAngle = FULLY_CLOSED_DEGREES 204 205 if (isFolded) { 206 hingeAngleProvider.stop() 207 notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED, lastHingeAngle) 208 cancelTimeout() 209 isUnfoldHandled = false 210 } else { 211 notifyFoldUpdate(FOLD_UPDATE_START_OPENING, lastHingeAngle) 212 rescheduleAbortAnimationTimeout() 213 hingeAngleProvider.start() 214 } 215 } 216 } 217 218 private inner class FoldRotationListener : RotationChangeProvider.RotationListener { 219 @WorkerThread onRotationChangednull220 override fun onRotationChanged(newRotation: Int) { 221 assertInProgressThread() 222 if (isTransitionInProgress) cancelAnimation() 223 } 224 } 225 notifyFoldUpdatenull226 private fun notifyFoldUpdate(@FoldStateProvider.FoldUpdate update: Int, angle: Float) { 227 if (DEBUG) { 228 Log.d(TAG, update.name()) 229 } 230 val previouslyTransitioning = isTransitionInProgress 231 232 outputListeners.forEach { it.onFoldUpdate(update) } 233 lastFoldUpdate = update 234 235 if (previouslyTransitioning != isTransitionInProgress) { 236 lastHingeAngleBeforeTransition = angle 237 } 238 } 239 rescheduleAbortAnimationTimeoutnull240 private fun rescheduleAbortAnimationTimeout() { 241 if (isTransitionInProgress) { 242 cancelTimeout() 243 } 244 progressHandler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong()) 245 } 246 cancelTimeoutnull247 private fun cancelTimeout() { 248 progressHandler.removeCallbacks(timeoutRunnable) 249 } 250 cancelAnimationnull251 private fun cancelAnimation(): Unit = 252 notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN, lastHingeAngle) 253 254 private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener { 255 256 override fun onScreenTurnedOn() { 257 executeInProgressThread { 258 // Trigger this event only if we are unfolded and this is the first screen 259 // turned on event since unfold started. This prevents running the animation when 260 // turning on the internal display using the power button. 261 // Initially isUnfoldHandled is true so it will be reset to false *only* when we 262 // receive 'folded' event. If SystemUI started when device is already folded it will 263 // still receive 'folded' event on startup. 264 if (!isFolded && !isUnfoldHandled) { 265 outputListeners.forEach { it.onUnfoldedScreenAvailable() } 266 isUnfoldHandled = true 267 } 268 } 269 } 270 271 override fun markScreenAsTurnedOn() { 272 executeInProgressThread { 273 if (!isFolded) { 274 isUnfoldHandled = true 275 } 276 } 277 } 278 279 override fun onScreenTurningOn() { 280 executeInProgressThread { 281 isScreenOn = true 282 updateHingeAngleProviderState() 283 } 284 } 285 286 override fun onScreenTurningOff() { 287 executeInProgressThread { 288 isScreenOn = false 289 updateHingeAngleProviderState() 290 } 291 } 292 293 /** 294 * Needed just for compatibility while not all data sources are providing data in the 295 * background. 296 * 297 * TODO(b/277879146): Remove once ScreeStatusProvider provides in the background. 298 */ 299 private fun executeInProgressThread(f: () -> Unit) { 300 progressHandler.post { f() } 301 } 302 } 303 isOnLargeScreennull304 private fun isOnLargeScreen(): Boolean { 305 return context.resources.configuration.smallestScreenWidthDp > 306 INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP 307 } 308 309 /** While the screen is off or the device is folded, hinge angle updates are not needed. */ updateHingeAngleProviderStatenull310 private fun updateHingeAngleProviderState() { 311 assertInProgressThread() 312 if (isScreenOn && !isFolded) { 313 hingeAngleProvider.start() 314 } else { 315 hingeAngleProvider.stop() 316 } 317 } 318 319 private inner class HingeAngleListener : Consumer<Float> { acceptnull320 override fun accept(angle: Float) { 321 assertInProgressThread() 322 onHingeAngle(angle) 323 } 324 } 325 assertInProgressThreadnull326 private fun assertInProgressThread() { 327 check(progressHandler.looper.isCurrentThread) { 328 val progressThread = progressHandler.looper.thread 329 val thisThread = Thread.currentThread() 330 """should be called from the progress thread. 331 progressThread=$progressThread tid=${progressThread.id} 332 Thread.currentThread()=$thisThread tid=${thisThread.id}""" 333 .trimMargin() 334 } 335 } 336 337 @AssistedFactory 338 interface Factory { 339 /** Creates a [DeviceFoldStateProvider] using the provided dependencies. */ createnull340 fun create( 341 hingeAngleProvider: HingeAngleProvider, 342 rotationChangeProvider: RotationChangeProvider, 343 progressHandler: Handler, 344 ): DeviceFoldStateProvider 345 } 346 } 347 348 fun @receiver:FoldStateProvider.FoldUpdate Int.name() = 349 when (this) { 350 FOLD_UPDATE_START_OPENING -> "START_OPENING" 351 FOLD_UPDATE_START_CLOSING -> "START_CLOSING" 352 FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN" 353 FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN" 354 FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED" 355 else -> "UNKNOWN" 356 } 357 358 private const val TAG = "DeviceFoldProvider" 359 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) 360 361 /** Threshold after which we consider the device fully unfolded. */ 362 @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f 363 364 /** Threshold after which hinge angle updates are considered. This is to eliminate noise. */ 365 @VisibleForTesting const val HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES = 7.5f 366 367 /** Fold animation on top of apps only when the angle exceeds this threshold. */ 368 @VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60 369