• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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