1 /* <lambda>null2 * Copyright (C) 2024 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 17 package com.android.systemui.unfold 18 19 import android.content.Context 20 import android.graphics.PixelFormat 21 import android.hardware.display.DisplayManager 22 import android.os.Handler 23 import android.os.Looper 24 import android.os.Trace 25 import android.view.Choreographer 26 import android.view.Display 27 import android.view.DisplayInfo 28 import android.view.Surface 29 import android.view.Surface.Rotation 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 androidx.annotation.WorkerThread 36 import com.android.app.tracing.traceSection 37 import com.android.systemui.dagger.qualifiers.Background 38 import com.android.systemui.dagger.qualifiers.Main 39 import com.android.systemui.settings.DisplayTracker 40 import com.android.systemui.statusbar.LightRevealEffect 41 import com.android.systemui.statusbar.LightRevealScrim 42 import com.android.systemui.unfold.dagger.UnfoldBg 43 import com.android.systemui.unfold.updates.RotationChangeProvider 44 import com.android.systemui.util.concurrency.ThreadFactory 45 import com.android.wm.shell.displayareahelper.DisplayAreaHelper 46 import dagger.assisted.Assisted 47 import dagger.assisted.AssistedFactory 48 import dagger.assisted.AssistedInject 49 import java.util.Optional 50 import java.util.concurrent.Executor 51 import java.util.function.Consumer 52 import kotlinx.coroutines.CoroutineScope 53 import kotlinx.coroutines.asCoroutineDispatcher 54 import com.android.app.tracing.coroutines.launchTraced as launch 55 56 interface FullscreenLightRevealAnimation { 57 fun init() 58 59 fun onScreenTurningOn(onOverlayReady: Runnable) 60 } 61 62 class FullscreenLightRevealAnimationController 63 @AssistedInject 64 constructor( 65 private val context: Context, 66 private val displayManager: DisplayManager, 67 private val threadFactory: ThreadFactory, 68 @UnfoldBg private val bgHandler: Handler, 69 @UnfoldBg private val rotationChangeProvider: RotationChangeProvider, 70 private val displayAreaHelper: Optional<DisplayAreaHelper>, 71 private val displayTracker: DisplayTracker, 72 @Background private val applicationScope: CoroutineScope, 73 @Main private val executor: Executor, 74 @Assisted private val displaySelector: List<DisplayInfo>.() -> DisplayInfo?, 75 @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect, 76 @Assisted private val overlayTitle: String 77 ) { 78 79 private lateinit var bgExecutor: Executor 80 private lateinit var wwm: WindowlessWindowManager 81 82 private var currentRotation: Int = context.display.rotation 83 private var root: SurfaceControlViewHost? = null 84 85 /** The scrim view that is used to reveal the screen. */ 86 var scrimView: LightRevealScrim? = null 87 private set 88 89 private val rotationWatcher = RotationWatcher() 90 private val internalDisplayInfos: List<DisplayInfo> = 91 displayManager 92 .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) <lambda>null93 .map { DisplayInfo().apply { it.getDisplayInfo(this) } } <lambda>null94 .filter { it.type == Display.TYPE_INTERNAL } 95 96 var isTouchBlocked: Boolean = false 97 set(value) { 98 if (value != field) { <lambda>null99 traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } 100 field = value 101 } 102 } 103 initnull104 fun init() { 105 bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler) 106 rotationChangeProvider.addCallback(rotationWatcher) 107 108 buildSurface { builder -> 109 applicationScope.launch(context = executor.asCoroutineDispatcher()) { 110 val overlayContainer = builder.build() 111 112 SurfaceControl.Transaction() 113 .setLayer(overlayContainer, OVERLAY_LAYER_Z_INDEX) 114 .show(overlayContainer) 115 .apply() 116 117 wwm = 118 WindowlessWindowManager(context.resources.configuration, overlayContainer, null) 119 } 120 } 121 } 122 addOverlaynull123 fun addOverlay( 124 initialAlpha: Float, 125 onOverlayReady: Runnable? = null, 126 ) { 127 if (!::wwm.isInitialized) { 128 // Surface overlay is not created yet on the first SysUI launch 129 onOverlayReady?.run() 130 return 131 } 132 ensureInBackground() 133 ensureOverlayRemoved() 134 prepareOverlay(onOverlayReady, wwm, bgExecutor, initialAlpha) 135 } 136 ensureOverlayRemovednull137 fun ensureOverlayRemoved() { 138 ensureInBackground() 139 140 traceSection("ensureOverlayRemoved") { 141 root?.release() 142 root = null 143 scrimView = null 144 } 145 } 146 isOverlayVisiblenull147 fun isOverlayVisible(): Boolean { 148 return scrimView == null 149 } 150 updateRevealAmountnull151 fun updateRevealAmount(revealAmount: Float) { 152 scrimView?.revealAmount = revealAmount 153 } 154 buildSurfacenull155 private fun buildSurface(onUpdated: Consumer<SurfaceControl.Builder>) { 156 val containerBuilder = 157 SurfaceControl.Builder(SurfaceSession()) 158 .setContainerLayer() 159 .setName("FoldUnfoldAnimationContainer") 160 161 displayAreaHelper 162 .get() 163 .attachToRootDisplayArea(displayTracker.defaultDisplayId, containerBuilder, onUpdated) 164 } 165 prepareOverlaynull166 private fun prepareOverlay( 167 onOverlayReady: Runnable? = null, 168 wwm: WindowlessWindowManager, 169 bgExecutor: Executor, 170 initialAlpha: Float, 171 ) { 172 val newRoot = SurfaceControlViewHost(context, context.display, wwm, javaClass.simpleName) 173 174 val params = getLayoutParams() 175 val newView = 176 LightRevealScrim( 177 context, 178 attrs = null, 179 initialWidth = params.width, 180 initialHeight = params.height 181 ) 182 .apply { 183 revealEffect = lightRevealEffectFactory(currentRotation) 184 revealAmount = initialAlpha 185 } 186 187 newRoot.setView(newView, params) 188 189 if (onOverlayReady != null) { 190 Trace.beginAsyncSection("$TAG#relayout", 0) 191 192 newRoot.relayout(params) { transaction -> 193 val vsyncId = Choreographer.getSfInstance().vsyncId 194 transaction.setFrameTimelineVsync(vsyncId).apply() 195 196 transaction 197 .setFrameTimelineVsync(vsyncId + 1) 198 .addTransactionCommittedListener(bgExecutor) { 199 Trace.endAsyncSection("$TAG#relayout", 0) 200 onOverlayReady.run() 201 } 202 .apply() 203 } 204 } 205 root = newRoot 206 scrimView = newView 207 } 208 ensureInBackgroundnull209 private fun ensureInBackground() { 210 check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } 211 } 212 getLayoutParamsnull213 private fun getLayoutParams(): WindowManager.LayoutParams { 214 val displayInfo = 215 internalDisplayInfos.displaySelector() 216 ?: throw IllegalArgumentException("No internal displays found!") 217 return WindowManager.LayoutParams().apply { 218 if (currentRotation.isVerticalRotation()) { 219 height = displayInfo.naturalHeight 220 width = displayInfo.naturalWidth 221 } else { 222 height = displayInfo.naturalWidth 223 width = displayInfo.naturalHeight 224 } 225 format = PixelFormat.TRANSLUCENT 226 type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY 227 title = overlayTitle 228 layoutInDisplayCutoutMode = 229 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 230 fitInsetsTypes = 0 231 232 flags = 233 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or 234 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 235 setTrustedOverlay() 236 237 packageName = context.opPackageName 238 } 239 } 240 241 private inner class RotationWatcher : RotationChangeProvider.RotationListener { 242 @WorkerThread onRotationChangednull243 override fun onRotationChanged(newRotation: Int) { 244 traceSection("$TAG#onRotationChanged") { 245 ensureInBackground() 246 if (currentRotation != newRotation) { 247 currentRotation = newRotation 248 scrimView?.revealEffect = lightRevealEffectFactory(currentRotation) 249 root?.relayout(getLayoutParams()) 250 } 251 } 252 } 253 } 254 255 @AssistedFactory 256 interface Factory { createnull257 fun create( 258 displaySelector: List<DisplayInfo>.() -> DisplayInfo?, 259 effectFactory: (rotation: Int) -> LightRevealEffect, 260 overlayContainerName: String 261 ): FullscreenLightRevealAnimationController 262 } 263 264 companion object { 265 private const val TAG = "FullscreenLightRevealAnimation" 266 private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE 267 private const val OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 268 const val ALPHA_TRANSPARENT = 1f 269 const val ALPHA_OPAQUE = 0f 270 271 fun @receiver:Rotation Int.isVerticalRotation(): Boolean = 272 this == Surface.ROTATION_0 || this == Surface.ROTATION_180 273 } 274 } 275