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 17 18 import android.annotation.BinderThread 19 import android.content.ContentResolver 20 import android.content.Context 21 import android.graphics.PixelFormat 22 import android.hardware.devicestate.DeviceStateManager 23 import android.hardware.devicestate.DeviceStateManager.FoldStateListener 24 import android.hardware.display.DisplayManager 25 import android.hardware.input.InputManagerGlobal 26 import android.os.Handler 27 import android.os.Looper 28 import android.os.Trace 29 import android.view.Choreographer 30 import android.view.Display 31 import android.view.DisplayInfo 32 import android.view.Surface 33 import android.view.SurfaceControl 34 import android.view.SurfaceControlViewHost 35 import android.view.SurfaceSession 36 import android.view.WindowManager 37 import android.view.WindowlessWindowManager 38 import com.android.systemui.dagger.qualifiers.Main 39 import com.android.systemui.flags.FeatureFlags 40 import com.android.systemui.flags.Flags 41 import com.android.systemui.settings.DisplayTracker 42 import com.android.systemui.statusbar.LightRevealEffect 43 import com.android.systemui.statusbar.LightRevealScrim 44 import com.android.systemui.statusbar.LinearLightRevealEffect 45 import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD 46 import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD 47 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener 48 import com.android.systemui.unfold.updates.RotationChangeProvider 49 import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled 50 import com.android.systemui.util.concurrency.ThreadFactory 51 import com.android.systemui.util.traceSection 52 import com.android.wm.shell.displayareahelper.DisplayAreaHelper 53 import java.util.Optional 54 import java.util.concurrent.Executor 55 import java.util.function.Consumer 56 import javax.inject.Inject 57 58 @SysUIUnfoldScope 59 class UnfoldLightRevealOverlayAnimation 60 @Inject 61 constructor( 62 private val context: Context, 63 private val featureFlags: FeatureFlags, 64 private val deviceStateManager: DeviceStateManager, 65 private val contentResolver: ContentResolver, 66 private val displayManager: DisplayManager, 67 private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider, 68 private val displayAreaHelper: Optional<DisplayAreaHelper>, 69 @Main private val executor: Executor, 70 private val threadFactory: ThreadFactory, 71 private val rotationChangeProvider: RotationChangeProvider, 72 private val displayTracker: DisplayTracker 73 ) { 74 75 private val transitionListener = TransitionListener() 76 private val rotationWatcher = RotationWatcher() 77 78 private lateinit var bgHandler: Handler 79 private lateinit var bgExecutor: Executor 80 81 private lateinit var wwm: WindowlessWindowManager 82 private lateinit var unfoldedDisplayInfo: DisplayInfo 83 private lateinit var overlayContainer: SurfaceControl 84 85 private var root: SurfaceControlViewHost? = null 86 private var scrimView: LightRevealScrim? = null 87 private var isFolded: Boolean = false 88 private var isUnfoldHandled: Boolean = true 89 private var overlayAddReason: AddOverlayReason? = null 90 private var isTouchBlocked: Boolean = true 91 92 private var currentRotation: Int = context.display!!.rotation 93 94 fun init() { 95 // This method will be called only on devices where this animation is enabled, 96 // so normally this thread won't be created 97 bgHandler = threadFactory.buildHandlerOnNewThread(TAG) 98 bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler) 99 100 deviceStateManager.registerCallback(bgExecutor, FoldListener()) 101 unfoldTransitionProgressProvider.addCallback(transitionListener) 102 rotationChangeProvider.addCallback(rotationWatcher) 103 104 val containerBuilder = 105 SurfaceControl.Builder(SurfaceSession()) 106 .setContainerLayer() 107 .setName("unfold-overlay-container") 108 109 displayAreaHelper.get().attachToRootDisplayArea( 110 displayTracker.defaultDisplayId, 111 containerBuilder 112 ) { builder -> 113 executor.execute { 114 overlayContainer = builder.build() 115 116 SurfaceControl.Transaction() 117 .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX) 118 .show(overlayContainer) 119 .apply() 120 121 wwm = 122 WindowlessWindowManager(context.resources.configuration, overlayContainer, null) 123 } 124 } 125 126 // Get unfolded display size immediately as 'current display info' might be 127 // not up-to-date during unfolding 128 unfoldedDisplayInfo = getUnfoldedDisplayInfo() 129 } 130 131 /** 132 * Called when screen starts turning on, the contents of the screen might not be visible yet. 133 * This method reports back that the overlay is ready in [onOverlayReady] callback. 134 * 135 * @param onOverlayReady callback when the overlay is drawn and visible on the screen 136 * @see [com.android.systemui.keyguard.KeyguardViewMediator] 137 */ 138 @BinderThread 139 fun onScreenTurningOn(onOverlayReady: Runnable) { 140 executeInBackground { 141 Trace.beginSection("$TAG#onScreenTurningOn") 142 try { 143 // Add the view only if we are unfolding and this is the first screen on 144 if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) { 145 addOverlay(onOverlayReady, reason = UNFOLD) 146 isUnfoldHandled = true 147 } else { 148 // No unfold transition, immediately report that overlay is ready 149 ensureOverlayRemoved() 150 onOverlayReady.run() 151 } 152 } finally { 153 Trace.endSection() 154 } 155 } 156 } 157 158 private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) { 159 if (!::wwm.isInitialized) { 160 // Surface overlay is not created yet on the first SysUI launch 161 onOverlayReady?.run() 162 return 163 } 164 165 ensureInBackground() 166 ensureOverlayRemoved() 167 168 overlayAddReason = reason 169 170 val newRoot = SurfaceControlViewHost(context, context.display!!, wwm, 171 "UnfoldLightRevealOverlayAnimation") 172 val params = getLayoutParams() 173 val newView = 174 LightRevealScrim( 175 context, 176 attrs = null, 177 initialWidth = params.width, 178 initialHeight = params.height 179 ) 180 .apply { 181 revealEffect = createLightRevealEffect() 182 isScrimOpaqueChangedListener = Consumer {} 183 revealAmount = calculateRevealAmount() 184 } 185 186 newRoot.setView(newView, params) 187 188 if (onOverlayReady != null) { 189 Trace.beginAsyncSection("$TAG#relayout", 0) 190 191 newRoot.relayout(params) { transaction -> 192 val vsyncId = Choreographer.getSfInstance().vsyncId 193 194 // Apply the transaction that contains the first frame of the overlay and apply 195 // another empty transaction with 'vsyncId + 1' to make sure that it is actually 196 // displayed on the screen. The second transaction is necessary to remove the screen 197 // blocker (turn on the brightness) only when the content is actually visible as it 198 // might be presented only in the next frame. 199 // See b/197538198 200 transaction.setFrameTimelineVsync(vsyncId).apply() 201 202 transaction 203 .setFrameTimelineVsync(vsyncId + 1) 204 .addTransactionCommittedListener(bgExecutor) { 205 Trace.endAsyncSection("$TAG#relayout", 0) 206 onOverlayReady.run() 207 } 208 .apply() 209 } 210 } 211 212 scrimView = newView 213 root = newRoot 214 } 215 216 private fun calculateRevealAmount(animationProgress: Float? = null): Float { 217 val overlayAddReason = overlayAddReason ?: UNFOLD 218 219 if (animationProgress == null) { 220 // Animation progress is unknown, calculate the initial value based on the overlay 221 // add reason 222 return when (overlayAddReason) { 223 FOLD -> TRANSPARENT 224 UNFOLD -> BLACK 225 } 226 } 227 228 val showVignetteWhenFolding = 229 featureFlags.isEnabled(Flags.ENABLE_DARK_VIGNETTE_WHEN_FOLDING) 230 231 return if (!showVignetteWhenFolding && overlayAddReason == FOLD) { 232 // Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off 233 // and we are folding the device. We still add the overlay to block touches 234 // while the animation is running but the overlay is transparent. 235 TRANSPARENT 236 } else { 237 animationProgress 238 } 239 } 240 241 private fun getLayoutParams(): WindowManager.LayoutParams { 242 val params: WindowManager.LayoutParams = WindowManager.LayoutParams() 243 244 val rotation = currentRotation 245 val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 246 247 params.height = 248 if (isNatural) unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth 249 params.width = 250 if (isNatural) unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight 251 252 params.format = PixelFormat.TRANSLUCENT 253 params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY 254 params.title = "Unfold Light Reveal Animation" 255 params.layoutInDisplayCutoutMode = 256 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 257 params.fitInsetsTypes = 0 258 259 val touchFlags = 260 if (isTouchBlocked) { 261 // Touchable by default, so it will block the touches 262 0 263 } else { 264 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 265 } 266 params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchFlags 267 params.setTrustedOverlay() 268 269 val packageName: String = context.opPackageName 270 params.packageName = packageName 271 272 return params 273 } 274 275 private fun updateTouchBlockIfNeeded(progress: Float) { 276 // When unfolding unblock touches a bit earlier than the animation end as the 277 // interpolation has a long tail of very slight movement at the end which should not 278 // affect much the usage of the device 279 val shouldBlockTouches = 280 if (overlayAddReason == UNFOLD) { 281 progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS 282 } else { 283 true 284 } 285 286 if (isTouchBlocked != shouldBlockTouches) { 287 isTouchBlocked = shouldBlockTouches 288 289 traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } 290 } 291 } 292 293 private fun createLightRevealEffect(): LightRevealEffect { 294 val isVerticalFold = 295 currentRotation == Surface.ROTATION_0 || currentRotation == Surface.ROTATION_180 296 return LinearLightRevealEffect(isVertical = isVerticalFold) 297 } 298 299 private fun ensureOverlayRemoved() { 300 ensureInBackground() 301 traceSection("ensureOverlayRemoved") { 302 root?.release() 303 root = null 304 scrimView = null 305 } 306 } 307 308 private fun getUnfoldedDisplayInfo(): DisplayInfo = 309 displayManager 310 .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) 311 .asSequence() 312 .map { DisplayInfo().apply { it.getDisplayInfo(this) } } 313 .filter { it.type == Display.TYPE_INTERNAL } 314 .maxByOrNull { it.naturalWidth }!! 315 316 private inner class TransitionListener : TransitionProgressListener { 317 318 override fun onTransitionProgress(progress: Float) { 319 executeInBackground { 320 scrimView?.revealAmount = calculateRevealAmount(progress) 321 updateTouchBlockIfNeeded(progress) 322 } 323 } 324 325 override fun onTransitionFinished() { 326 executeInBackground { ensureOverlayRemoved() } 327 } 328 329 override fun onTransitionStarted() { 330 // Add view for folding case (when unfolding the view is added earlier) 331 if (scrimView == null) { 332 executeInBackground { addOverlay(reason = FOLD) } 333 } 334 // Disable input dispatching during transition. 335 InputManagerGlobal.getInstance().cancelCurrentTouch() 336 } 337 } 338 339 private inner class RotationWatcher : RotationChangeProvider.RotationListener { 340 override fun onRotationChanged(newRotation: Int) { 341 executeInBackground { 342 traceSection("$TAG#onRotationChanged") { 343 if (currentRotation != newRotation) { 344 currentRotation = newRotation 345 scrimView?.revealEffect = createLightRevealEffect() 346 root?.relayout(getLayoutParams()) 347 } 348 } 349 } 350 } 351 } 352 353 private fun executeInBackground(f: () -> Unit) { 354 check(Looper.myLooper() != bgHandler.looper) { 355 "Trying to execute using background handler while already running" + 356 " in the background handler" 357 } 358 // The UiBackground executor is not used as it doesn't have a prepared looper. 359 bgHandler.post(f) 360 } 361 362 private fun ensureInBackground() { 363 check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } 364 } 365 366 private inner class FoldListener : 367 FoldStateListener( 368 context, 369 Consumer { isFolded -> 370 if (isFolded) { 371 ensureOverlayRemoved() 372 isUnfoldHandled = false 373 } 374 this.isFolded = isFolded 375 } 376 ) 377 378 private enum class AddOverlayReason { 379 FOLD, 380 UNFOLD 381 } 382 383 private companion object { 384 const val TAG = "UnfoldLightRevealOverlayAnimation" 385 const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE 386 387 // Put the unfold overlay below the rotation animation screenshot to hide the moment 388 // when it is rotated but the rotation of the other windows hasn't happen yet 389 const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 390 391 // constants for revealAmount. 392 const val TRANSPARENT = 1f 393 const val BLACK = 0f 394 395 private const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f 396 } 397 } 398