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.animation.ValueAnimator
19 import android.content.Context
20 import android.graphics.PixelFormat
21 import android.hardware.devicestate.DeviceStateManager
22 import android.hardware.devicestate.DeviceStateManager.FoldStateListener
23 import android.hardware.display.DisplayManager
24 import android.os.Handler
25 import android.os.Trace
26 import android.view.Choreographer
27 import android.view.Display
28 import android.view.DisplayInfo
29 import android.view.Surface
30 import android.view.SurfaceControl
31 import android.view.SurfaceControlViewHost
32 import android.view.SurfaceSession
33 import android.view.WindowManager
34 import android.view.WindowlessWindowManager
35 import com.android.systemui.dagger.qualifiers.Main
36 import com.android.systemui.dagger.qualifiers.UiBackground
37 import com.android.systemui.statusbar.LightRevealEffect
38 import com.android.systemui.statusbar.LightRevealScrim
39 import com.android.systemui.statusbar.LinearLightRevealEffect
40 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
41 import com.android.wm.shell.displayareahelper.DisplayAreaHelper
42 import java.util.Optional
43 import java.util.concurrent.Executor
44 import java.util.function.Consumer
45 import javax.inject.Inject
46 
47 @SysUIUnfoldScope
48 class UnfoldLightRevealOverlayAnimation @Inject constructor(
49     private val context: Context,
50     private val deviceStateManager: DeviceStateManager,
51     private val displayManager: DisplayManager,
52     private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
53     private val displayAreaHelper: Optional<DisplayAreaHelper>,
54     @Main private val executor: Executor,
55     @Main private val handler: Handler,
56     @UiBackground private val backgroundExecutor: Executor
57 ) {
58 
59     private val transitionListener = TransitionListener()
60     private val displayListener = DisplayChangeListener()
61 
62     private lateinit var wwm: WindowlessWindowManager
63     private lateinit var unfoldedDisplayInfo: DisplayInfo
64     private lateinit var overlayContainer: SurfaceControl
65 
66     private var root: SurfaceControlViewHost? = null
67     private var scrimView: LightRevealScrim? = null
68     private var isFolded: Boolean = false
69     private var isUnfoldHandled: Boolean = true
70 
71     private var currentRotation: Int = context.display!!.rotation
72 
73     fun init() {
74         deviceStateManager.registerCallback(executor, FoldListener())
75         unfoldTransitionProgressProvider.addCallback(transitionListener)
76 
77         val containerBuilder = SurfaceControl.Builder(SurfaceSession())
78             .setContainerLayer()
79             .setName("unfold-overlay-container")
80 
81         displayAreaHelper.get().attachToRootDisplayArea(Display.DEFAULT_DISPLAY,
82             containerBuilder) { builder ->
83             executor.execute {
84                 overlayContainer = builder.build()
85 
86                 SurfaceControl.Transaction()
87                     .setLayer(overlayContainer, Integer.MAX_VALUE)
88                     .show(overlayContainer)
89                     .apply()
90 
91                 wwm = WindowlessWindowManager(context.resources.configuration,
92                     overlayContainer, null)
93             }
94         }
95 
96         displayManager.registerDisplayListener(displayListener, handler,
97             DisplayManager.EVENT_FLAG_DISPLAY_CHANGED)
98 
99         // Get unfolded display size immediately as 'current display info' might be
100         // not up-to-date during unfolding
101         unfoldedDisplayInfo = getUnfoldedDisplayInfo()
102     }
103 
104     /**
105      * Called when screen starts turning on, the contents of the screen might not be visible yet.
106      * This method reports back that the overlay is ready in [onOverlayReady] callback.
107      *
108      * @param onOverlayReady callback when the overlay is drawn and visible on the screen
109      * @see [com.android.systemui.keyguard.KeyguardViewMediator]
110      */
111     fun onScreenTurningOn(onOverlayReady: Runnable) {
112         Trace.beginSection("UnfoldLightRevealOverlayAnimation#onScreenTurningOn")
113         try {
114             // Add the view only if we are unfolding and this is the first screen on
115             if (!isFolded && !isUnfoldHandled && ValueAnimator.areAnimatorsEnabled()) {
116                 addView(onOverlayReady)
117                 isUnfoldHandled = true
118             } else {
119                 // No unfold transition, immediately report that overlay is ready
120                 ensureOverlayRemoved()
121                 onOverlayReady.run()
122             }
123         } finally {
124             Trace.endSection()
125         }
126     }
127 
128     private fun addView(onOverlayReady: Runnable? = null) {
129         if (!::wwm.isInitialized) {
130             // Surface overlay is not created yet on the first SysUI launch
131             onOverlayReady?.run()
132             return
133         }
134 
135         ensureOverlayRemoved()
136 
137         val newRoot = SurfaceControlViewHost(context, context.display!!, wwm, false)
138         val newView = LightRevealScrim(context, null)
139             .apply {
140                 revealEffect = createLightRevealEffect()
141                 isScrimOpaqueChangedListener = Consumer {}
142                 revealAmount = 0f
143             }
144 
145         val params = getLayoutParams()
146         newRoot.setView(newView, params)
147 
148         onOverlayReady?.let { callback ->
149             Trace.beginAsyncSection(
150                 "UnfoldLightRevealOverlayAnimation#relayout", 0)
151 
152             newRoot.relayout(params) { transaction ->
153                 val vsyncId = Choreographer.getSfInstance().vsyncId
154 
155                 backgroundExecutor.execute {
156                     // Apply the transaction that contains the first frame of the overlay
157                     // synchronously and apply another empty transaction with
158                     // 'vsyncId + 1' to make sure that it is actually displayed on
159                     // the screen. The second transaction is necessary to remove the screen blocker
160                     // (turn on the brightness) only when the content is actually visible as it
161                     // might be presented only in the next frame.
162                     // See b/197538198
163                     transaction.setFrameTimelineVsync(vsyncId)
164                         .apply(/* sync */true)
165 
166                     transaction
167                         .setFrameTimelineVsync(vsyncId + 1)
168                         .apply(/* sync */ true)
169 
170                     Trace.endAsyncSection(
171                         "UnfoldLightRevealOverlayAnimation#relayout", 0)
172                     callback.run()
173                 }
174             }
175         }
176 
177         scrimView = newView
178         root = newRoot
179     }
180 
181     private fun getLayoutParams(): WindowManager.LayoutParams {
182         val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
183 
184         val rotation = context.display!!.rotation
185         val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
186 
187         params.height = if (isNatural)
188             unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth
189         params.width = if (isNatural)
190             unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight
191 
192         params.format = PixelFormat.TRANSLUCENT
193         params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
194         params.title = "Unfold Light Reveal Animation"
195         params.layoutInDisplayCutoutMode =
196             WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
197         params.fitInsetsTypes = 0
198         params.flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
199             or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
200         params.setTrustedOverlay()
201 
202         val packageName: String = context.opPackageName
203         params.packageName = packageName
204 
205         return params
206     }
207 
208     private fun createLightRevealEffect(): LightRevealEffect {
209         val isVerticalFold = currentRotation == Surface.ROTATION_0 ||
210             currentRotation == Surface.ROTATION_180
211         return LinearLightRevealEffect(isVertical = isVerticalFold)
212     }
213 
214     private fun ensureOverlayRemoved() {
215         root?.release()
216         root = null
217         scrimView = null
218     }
219 
220     private fun getUnfoldedDisplayInfo(): DisplayInfo =
221         displayManager.displays
222             .asSequence()
223             .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
224             .filter { it.type == Display.TYPE_INTERNAL }
225             .maxByOrNull { it.naturalWidth }!!
226 
227     private inner class TransitionListener : TransitionProgressListener {
228 
229         override fun onTransitionProgress(progress: Float) {
230             scrimView?.revealAmount = progress
231         }
232 
233         override fun onTransitionFinished() {
234             ensureOverlayRemoved()
235         }
236 
237         override fun onTransitionStarted() {
238             // Add view for folding case (when unfolding the view is added earlier)
239             if (scrimView == null) {
240                 addView()
241             }
242         }
243     }
244 
245     private inner class DisplayChangeListener : DisplayManager.DisplayListener {
246 
247         override fun onDisplayChanged(displayId: Int) {
248             val newRotation: Int = context.display!!.rotation
249             if (currentRotation != newRotation) {
250                 currentRotation = newRotation
251                 scrimView?.revealEffect = createLightRevealEffect()
252                 root?.relayout(getLayoutParams())
253             }
254         }
255 
256         override fun onDisplayAdded(displayId: Int) {
257         }
258 
259         override fun onDisplayRemoved(displayId: Int) {
260         }
261     }
262 
263     private inner class FoldListener : FoldStateListener(context, Consumer { isFolded ->
264         if (isFolded) {
265             ensureOverlayRemoved()
266             isUnfoldHandled = false
267         }
268         this.isFolded = isFolded
269     })
270 }
271