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