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