1 /* <lambda>null2 * 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.InputManager 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 val params = getLayoutParams() 172 val newView = 173 LightRevealScrim( 174 context, 175 attrs = null, 176 initialWidth = params.width, 177 initialHeight = params.height 178 ) 179 .apply { 180 revealEffect = createLightRevealEffect() 181 isScrimOpaqueChangedListener = Consumer {} 182 revealAmount = calculateRevealAmount() 183 } 184 185 newRoot.setView(newView, params) 186 187 if (onOverlayReady != null) { 188 Trace.beginAsyncSection("$TAG#relayout", 0) 189 190 newRoot.relayout(params) { transaction -> 191 val vsyncId = Choreographer.getSfInstance().vsyncId 192 193 // Apply the transaction that contains the first frame of the overlay and apply 194 // another empty transaction with 'vsyncId + 1' to make sure that it is actually 195 // displayed on the screen. The second transaction is necessary to remove the screen 196 // blocker (turn on the brightness) only when the content is actually visible as it 197 // might be presented only in the next frame. 198 // See b/197538198 199 transaction.setFrameTimelineVsync(vsyncId).apply() 200 201 transaction 202 .setFrameTimelineVsync(vsyncId + 1) 203 .addTransactionCommittedListener(bgExecutor) { 204 Trace.endAsyncSection("$TAG#relayout", 0) 205 onOverlayReady.run() 206 } 207 .apply() 208 } 209 } 210 211 scrimView = newView 212 root = newRoot 213 } 214 215 private fun calculateRevealAmount(animationProgress: Float? = null): Float { 216 val overlayAddReason = overlayAddReason ?: UNFOLD 217 218 if (animationProgress == null) { 219 // Animation progress is unknown, calculate the initial value based on the overlay 220 // add reason 221 return when (overlayAddReason) { 222 FOLD -> TRANSPARENT 223 UNFOLD -> BLACK 224 } 225 } 226 227 val showVignetteWhenFolding = 228 featureFlags.isEnabled(Flags.ENABLE_DARK_VIGNETTE_WHEN_FOLDING) 229 230 return if (!showVignetteWhenFolding && overlayAddReason == FOLD) { 231 // Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off 232 // and we are folding the device. We still add the overlay to block touches 233 // while the animation is running but the overlay is transparent. 234 TRANSPARENT 235 } else { 236 animationProgress 237 } 238 } 239 240 private fun getLayoutParams(): WindowManager.LayoutParams { 241 val params: WindowManager.LayoutParams = WindowManager.LayoutParams() 242 243 val rotation = currentRotation 244 val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 245 246 params.height = 247 if (isNatural) unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth 248 params.width = 249 if (isNatural) unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight 250 251 params.format = PixelFormat.TRANSLUCENT 252 params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY 253 params.title = "Unfold Light Reveal Animation" 254 params.layoutInDisplayCutoutMode = 255 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 256 params.fitInsetsTypes = 0 257 258 val touchFlags = 259 if (isTouchBlocked) { 260 // Touchable by default, so it will block the touches 261 0 262 } else { 263 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 264 } 265 params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchFlags 266 params.setTrustedOverlay() 267 268 val packageName: String = context.opPackageName 269 params.packageName = packageName 270 271 return params 272 } 273 274 private fun updateTouchBlockIfNeeded(progress: Float) { 275 // When unfolding unblock touches a bit earlier than the animation end as the 276 // interpolation has a long tail of very slight movement at the end which should not 277 // affect much the usage of the device 278 val shouldBlockTouches = 279 if (overlayAddReason == UNFOLD) { 280 progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS 281 } else { 282 true 283 } 284 285 if (isTouchBlocked != shouldBlockTouches) { 286 isTouchBlocked = shouldBlockTouches 287 288 traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } 289 } 290 } 291 292 private fun createLightRevealEffect(): LightRevealEffect { 293 val isVerticalFold = 294 currentRotation == Surface.ROTATION_0 || currentRotation == Surface.ROTATION_180 295 return LinearLightRevealEffect(isVertical = isVerticalFold) 296 } 297 298 private fun ensureOverlayRemoved() { 299 ensureInBackground() 300 traceSection("ensureOverlayRemoved") { 301 root?.release() 302 root = null 303 scrimView = null 304 } 305 } 306 307 private fun getUnfoldedDisplayInfo(): DisplayInfo = 308 displayManager 309 .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) 310 .asSequence() 311 .map { DisplayInfo().apply { it.getDisplayInfo(this) } } 312 .filter { it.type == Display.TYPE_INTERNAL } 313 .maxByOrNull { it.naturalWidth }!! 314 315 private inner class TransitionListener : TransitionProgressListener { 316 317 override fun onTransitionProgress(progress: Float) { 318 executeInBackground { 319 scrimView?.revealAmount = calculateRevealAmount(progress) 320 updateTouchBlockIfNeeded(progress) 321 } 322 } 323 324 override fun onTransitionFinished() { 325 executeInBackground { ensureOverlayRemoved() } 326 } 327 328 override fun onTransitionStarted() { 329 // Add view for folding case (when unfolding the view is added earlier) 330 if (scrimView == null) { 331 executeInBackground { addOverlay(reason = FOLD) } 332 } 333 // Disable input dispatching during transition. 334 InputManager.getInstance().cancelCurrentTouch() 335 } 336 } 337 338 private inner class RotationWatcher : RotationChangeProvider.RotationListener { 339 override fun onRotationChanged(newRotation: Int) { 340 executeInBackground { 341 traceSection("$TAG#onRotationChanged") { 342 if (currentRotation != newRotation) { 343 currentRotation = newRotation 344 scrimView?.revealEffect = createLightRevealEffect() 345 root?.relayout(getLayoutParams()) 346 } 347 } 348 } 349 } 350 } 351 352 private fun executeInBackground(f: () -> Unit) { 353 check(Looper.myLooper() != bgHandler.looper) { 354 "Trying to execute using background handler while already running" + 355 " in the background handler" 356 } 357 // The UiBackground executor is not used as it doesn't have a prepared looper. 358 bgHandler.post(f) 359 } 360 361 private fun ensureInBackground() { 362 check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } 363 } 364 365 private inner class FoldListener : 366 FoldStateListener( 367 context, 368 Consumer { isFolded -> 369 if (isFolded) { 370 ensureOverlayRemoved() 371 isUnfoldHandled = false 372 } 373 this.isFolded = isFolded 374 } 375 ) 376 377 private enum class AddOverlayReason { 378 FOLD, 379 UNFOLD 380 } 381 382 private companion object { 383 const val TAG = "UnfoldLightRevealOverlayAnimation" 384 const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE 385 386 // Put the unfold overlay below the rotation animation screenshot to hide the moment 387 // when it is rotated but the rotation of the other windows hasn't happen yet 388 const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 389 390 // constants for revealAmount. 391 const val TRANSPARENT = 1f 392 const val BLACK = 0f 393 394 private const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f 395 } 396 } 397