Loading packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt +19 −15 Original line number Diff line number Diff line Loading @@ -29,26 +29,28 @@ import java.util.Optional import javax.inject.Inject /** * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for * screen on events, this will invoke the onDrawn Runnable after all tasks have completed. This * should route back to the [com.android.systemui.keyguard.KeyguardService], which informs * the system_server that keyguard has drawn. * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for screen * on events, this will invoke the onDrawn Runnable after all tasks have completed. This should * route back to the [com.android.systemui.keyguard.KeyguardService], which informs the * system_server that keyguard has drawn. */ @SysUISingleton class ScreenOnCoordinator @Inject constructor( class ScreenOnCoordinator @Inject constructor( unfoldComponent: Optional<SysUIUnfoldComponent>, @Main private val mainHandler: Handler @Main private val mainHandler: Handler, ) { private val unfoldLightRevealAnimation = unfoldComponent.map( SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation).getOrNull() private val foldAodAnimationController = unfoldComponent.map( SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull() private val foldAodAnimationController = unfoldComponent.map(SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull() private val fullScreenLightRevealAnimations = unfoldComponent.map(SysUIUnfoldComponent::getFullScreenLightRevealAnimations).getOrNull() private val pendingTasks = PendingTasksContainer() /** * When turning on, registers tasks that may need to run before invoking [onDrawn]. * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. * When turning on, registers tasks that may need to run before invoking [onDrawn]. This is * called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. */ @BinderThread fun onScreenTurningOn(onDrawn: Runnable) { Loading @@ -56,8 +58,10 @@ class ScreenOnCoordinator @Inject constructor( pendingTasks.reset() unfoldLightRevealAnimation?.onScreenTurningOn(pendingTasks.registerTask("unfold-reveal")) foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod")) fullScreenLightRevealAnimations?.forEach { it.onScreenTurningOn(pendingTasks.registerTask(it::class.java.simpleName)) } pendingTasks.onTasksComplete { if (Flags.enableBackgroundKeyguardOndrawnCallback()) { Loading @@ -71,8 +75,8 @@ class ScreenOnCoordinator @Inject constructor( } /** * Called when screen is fully turned on and screen on blocker is removed. * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. * Called when screen is fully turned on and screen on blocker is removed. This is called on a * binder thread from [com.android.systemui.keyguard.KeyguardService]. */ @BinderThread fun onScreenTurnedOn() { Loading packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +1 −1 Original line number Diff line number Diff line Loading @@ -134,7 +134,7 @@ public interface SysUIComponent { getSysUIUnfoldComponent() .ifPresent( c -> { c.getUnfoldLightRevealOverlayAnimation().init(); c.getFullScreenLightRevealAnimations().forEach(it -> it.init()); c.getUnfoldTransitionWallpaperController().init(); c.getUnfoldHapticsPlayer(); c.getNaturalRotationUnfoldProgressProvider().init(); Loading packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +48 −0 Original line number Diff line number Diff line Loading @@ -156,6 +156,54 @@ data class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevea } } data class LinearSideLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect { override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { scrim.interpolatedRevealAmount = amount scrim.startColorAlpha = getPercentPastThreshold(1 - amount, threshold = 1 - START_COLOR_REVEAL_PERCENTAGE) scrim.revealGradientEndColorAlpha = 1f - getPercentPastThreshold( amount, threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE ) val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1f, amount) if (isVertical) { scrim.setRevealGradientBounds( left = -(scrim.viewWidth) * gradientBoundsAmount, top = -(scrim.viewHeight) * gradientBoundsAmount, right = (scrim.viewWidth) * gradientBoundsAmount, bottom = (scrim.viewHeight) + (scrim.viewHeight) * gradientBoundsAmount ) } else { scrim.setRevealGradientBounds( left = -(scrim.viewWidth) * gradientBoundsAmount, top = -(scrim.viewHeight) * gradientBoundsAmount, right = (scrim.viewWidth) + (scrim.viewWidth) * gradientBoundsAmount, bottom = (scrim.viewHeight) * gradientBoundsAmount ) } } private companion object { // From which percentage we should start the gradient reveal width // E.g. if 0 - starts with 0px width, 0.6f - starts with 60% width private const val GRADIENT_START_BOUNDS_PERCENTAGE: Float = 1f // When to start changing alpha color of the gradient scrim // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely // transparent at 100% private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE: Float = 1f // When to finish displaying start color fill that reveals the content // E.g. if 0.6f - the content won't be visible at 0% and it will gradually // reduce the alpha until 60% (at this point the color fill is invisible) private const val START_COLOR_REVEAL_PERCENTAGE: Float = 1f } } data class CircleReveal( /** X-value of the circle center of the reveal. */ val centerX: Int, Loading packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt 0 → 100644 +153 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.unfold import android.animation.ValueAnimator import android.annotation.BinderThread import android.content.Context import android.os.Handler import android.os.SystemProperties import android.util.Log import android.view.animation.DecelerateInterpolator import androidx.core.animation.addListener import com.android.internal.foldables.FoldLockSettingAvailabilityProvider import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.statusbar.LinearSideLightRevealEffect import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation import com.android.systemui.unfold.dagger.UnfoldBg import com.android.systemui.util.animation.data.repository.AnimationStatusRepository import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout class FoldLightRevealOverlayAnimation @Inject constructor( private val context: Context, @UnfoldBg private val bgHandler: Handler, private val deviceStateRepository: DeviceStateRepository, private val powerInteractor: PowerInteractor, @Background private val applicationScope: CoroutineScope, private val animationStatusRepository: AnimationStatusRepository, private val controllerFactory: FullscreenLightRevealAnimationController.Factory ) : FullscreenLightRevealAnimation { private val revealProgressValueAnimator: ValueAnimator = ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT) private lateinit var controller: FullscreenLightRevealAnimationController @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null override fun init() { // This method will be called only on devices where this animation is enabled, // so normally this thread won't be created if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) { return } controller = controllerFactory.create( displaySelector = { minByOrNull { it.naturalWidth } }, effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) }, overlayContainerName = SURFACE_CONTAINER_NAME ) controller.init() applicationScope.launch(bgHandler.asCoroutineDispatcher()) { powerInteractor.screenPowerState.collect { if (it == ScreenPowerState.SCREEN_ON) { readyCallback = null } } } applicationScope.launch(bgHandler.asCoroutineDispatcher()) { deviceStateRepository.state .map { it != DeviceStateRepository.DeviceState.FOLDED } .distinctUntilChanged() .filter { isUnfolded -> isUnfolded } .collect { controller.ensureOverlayRemoved() } } applicationScope.launch(bgHandler.asCoroutineDispatcher()) { deviceStateRepository.state .filter { animationStatusRepository.areAnimationsEnabled().first() && it == DeviceStateRepository.DeviceState.FOLDED } .collect { try { withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { readyCallback = CompletableDeferred() val onReady = readyCallback?.await() readyCallback = null controller.addOverlay(ALPHA_OPAQUE, onReady) waitForScreenTurnedOn() playFoldLightRevealOverlayAnimation() } } catch (e: TimeoutCancellationException) { Log.e(TAG, "Fold light reveal animation timed out") ensureOverlayRemovedInternal() } } } } @BinderThread override fun onScreenTurningOn(onOverlayReady: Runnable) { readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run() } private suspend fun waitForScreenTurnedOn() { powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() } private fun ensureOverlayRemovedInternal() { revealProgressValueAnimator.cancel() controller.ensureOverlayRemoved() } private fun playFoldLightRevealOverlayAnimation() { revealProgressValueAnimator.duration = ANIMATION_DURATION revealProgressValueAnimator.interpolator = DecelerateInterpolator() revealProgressValueAnimator.addUpdateListener { animation -> controller.updateRevealAmount(animation.animatedFraction) } revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() }) revealProgressValueAnimator.start() } private companion object { const val TAG = "FoldLightRevealOverlayAnimation" const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L const val SURFACE_CONTAINER_NAME = "fold-overlay-container" val ANIMATION_DURATION: Long get() = SystemProperties.getLong("persist.fold_animation_duration", 200L) } } packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt 0 → 100644 +271 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.unfold import android.content.Context import android.graphics.PixelFormat import android.hardware.display.DisplayManager import android.os.Handler import android.os.Looper import android.os.Trace import android.view.Choreographer import android.view.Display import android.view.DisplayInfo import android.view.Surface import android.view.Surface.Rotation import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.SurfaceSession import android.view.WindowManager import android.view.WindowlessWindowManager import com.android.app.tracing.traceSection import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.DisplayTracker import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.unfold.dagger.UnfoldBg import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.util.concurrency.ThreadFactory import com.android.wm.shell.displayareahelper.DisplayAreaHelper import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.lang.IllegalArgumentException import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch interface FullscreenLightRevealAnimation { fun init() fun onScreenTurningOn(onOverlayReady: Runnable) } class FullscreenLightRevealAnimationController @AssistedInject constructor( private val context: Context, private val displayManager: DisplayManager, private val threadFactory: ThreadFactory, @UnfoldBg private val bgHandler: Handler, @UnfoldBg private val rotationChangeProvider: RotationChangeProvider, private val displayAreaHelper: Optional<DisplayAreaHelper>, private val displayTracker: DisplayTracker, @Background private val applicationScope: CoroutineScope, @Main private val executor: Executor, @Assisted private val displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?, @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect, @Assisted private val overlayContainerName: String ) { private lateinit var bgExecutor: Executor private lateinit var wwm: WindowlessWindowManager private var currentRotation: Int = context.display.rotation private var root: SurfaceControlViewHost? = null private var scrimView: LightRevealScrim? = null private val rotationWatcher = RotationWatcher() private val internalDisplayInfos: Sequence<DisplayInfo> get() = displayManager .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .asSequence() .map { DisplayInfo().apply { it.getDisplayInfo(this) } } .filter { it.type == Display.TYPE_INTERNAL } var isTouchBlocked: Boolean = false set(value) { if (value != field) { traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } field = value } } fun init() { bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler) rotationChangeProvider.addCallback(rotationWatcher) buildSurface { builder -> applicationScope.launch(executor.asCoroutineDispatcher()) { val overlayContainer = builder.build() SurfaceControl.Transaction() .setLayer(overlayContainer, OVERLAY_LAYER_Z_INDEX) .show(overlayContainer) .apply() wwm = WindowlessWindowManager(context.resources.configuration, overlayContainer, null) } } } fun addOverlay( initialAlpha: Float, onOverlayReady: Runnable? = null, ) { if (!::wwm.isInitialized) { // Surface overlay is not created yet on the first SysUI launch onOverlayReady?.run() return } ensureInBackground() ensureOverlayRemoved() prepareOverlay(onOverlayReady, wwm, bgExecutor, initialAlpha) } fun ensureOverlayRemoved() { ensureInBackground() traceSection("ensureOverlayRemoved") { root?.release() root = null scrimView = null } } fun isOverlayVisible(): Boolean { return scrimView == null } fun updateRevealAmount(revealAmount: Float) { scrimView?.revealAmount = revealAmount } private fun buildSurface(onUpdated: Consumer<SurfaceControl.Builder>) { val containerBuilder = SurfaceControl.Builder(SurfaceSession()) .setContainerLayer() .setName(overlayContainerName) displayAreaHelper .get() .attachToRootDisplayArea(displayTracker.defaultDisplayId, containerBuilder, onUpdated) } private fun prepareOverlay( onOverlayReady: Runnable? = null, wwm: WindowlessWindowManager, bgExecutor: Executor, initialAlpha: Float, ) { val newRoot = SurfaceControlViewHost(context, context.display, wwm, javaClass.simpleName) val params = getLayoutParams() val newView = LightRevealScrim( context, attrs = null, initialWidth = params.width, initialHeight = params.height ) .apply { revealEffect = lightRevealEffectFactory(currentRotation) revealAmount = initialAlpha } newRoot.setView(newView, params) if (onOverlayReady != null) { Trace.beginAsyncSection("$TAG#relayout", 0) newRoot.relayout(params) { transaction -> val vsyncId = Choreographer.getSfInstance().vsyncId transaction.setFrameTimelineVsync(vsyncId).apply() transaction .setFrameTimelineVsync(vsyncId + 1) .addTransactionCommittedListener(bgExecutor) { Trace.endAsyncSection("$TAG#relayout", 0) onOverlayReady.run() } .apply() } } root = newRoot scrimView = newView } private fun ensureInBackground() { check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } } private fun getLayoutParams(): WindowManager.LayoutParams { val displayInfo = internalDisplayInfos.displaySelector() ?: throw IllegalArgumentException("No internal displays found!") return WindowManager.LayoutParams().apply { if (currentRotation.isVerticalRotation()) { height = displayInfo.naturalHeight width = displayInfo.naturalWidth } else { height = displayInfo.naturalWidth width = displayInfo.naturalHeight } format = PixelFormat.TRANSLUCENT type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY title = javaClass.simpleName layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS fitInsetsTypes = 0 flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE setTrustedOverlay() packageName = context.opPackageName } } private inner class RotationWatcher : RotationChangeProvider.RotationListener { override fun onRotationChanged(newRotation: Int) { traceSection("$TAG#onRotationChanged") { if (currentRotation != newRotation) { currentRotation = newRotation scrimView?.revealEffect = lightRevealEffectFactory(currentRotation) root?.relayout(getLayoutParams()) } } } } @AssistedFactory interface Factory { fun create( displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?, effectFactory: (rotation: Int) -> LightRevealEffect, overlayContainerName: String ): FullscreenLightRevealAnimationController } companion object { private const val TAG = "FullscreenLightRevealAnimation" private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE private const val OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 const val ALPHA_TRANSPARENT = 1f const val ALPHA_OPAQUE = 0f fun @receiver:Rotation Int.isVerticalRotation(): Boolean = this == Surface.ROTATION_0 || this == Surface.ROTATION_180 } } Loading
packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt +19 −15 Original line number Diff line number Diff line Loading @@ -29,26 +29,28 @@ import java.util.Optional import javax.inject.Inject /** * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for * screen on events, this will invoke the onDrawn Runnable after all tasks have completed. This * should route back to the [com.android.systemui.keyguard.KeyguardService], which informs * the system_server that keyguard has drawn. * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for screen * on events, this will invoke the onDrawn Runnable after all tasks have completed. This should * route back to the [com.android.systemui.keyguard.KeyguardService], which informs the * system_server that keyguard has drawn. */ @SysUISingleton class ScreenOnCoordinator @Inject constructor( class ScreenOnCoordinator @Inject constructor( unfoldComponent: Optional<SysUIUnfoldComponent>, @Main private val mainHandler: Handler @Main private val mainHandler: Handler, ) { private val unfoldLightRevealAnimation = unfoldComponent.map( SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation).getOrNull() private val foldAodAnimationController = unfoldComponent.map( SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull() private val foldAodAnimationController = unfoldComponent.map(SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull() private val fullScreenLightRevealAnimations = unfoldComponent.map(SysUIUnfoldComponent::getFullScreenLightRevealAnimations).getOrNull() private val pendingTasks = PendingTasksContainer() /** * When turning on, registers tasks that may need to run before invoking [onDrawn]. * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. * When turning on, registers tasks that may need to run before invoking [onDrawn]. This is * called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. */ @BinderThread fun onScreenTurningOn(onDrawn: Runnable) { Loading @@ -56,8 +58,10 @@ class ScreenOnCoordinator @Inject constructor( pendingTasks.reset() unfoldLightRevealAnimation?.onScreenTurningOn(pendingTasks.registerTask("unfold-reveal")) foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod")) fullScreenLightRevealAnimations?.forEach { it.onScreenTurningOn(pendingTasks.registerTask(it::class.java.simpleName)) } pendingTasks.onTasksComplete { if (Flags.enableBackgroundKeyguardOndrawnCallback()) { Loading @@ -71,8 +75,8 @@ class ScreenOnCoordinator @Inject constructor( } /** * Called when screen is fully turned on and screen on blocker is removed. * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. * Called when screen is fully turned on and screen on blocker is removed. This is called on a * binder thread from [com.android.systemui.keyguard.KeyguardService]. */ @BinderThread fun onScreenTurnedOn() { Loading
packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +1 −1 Original line number Diff line number Diff line Loading @@ -134,7 +134,7 @@ public interface SysUIComponent { getSysUIUnfoldComponent() .ifPresent( c -> { c.getUnfoldLightRevealOverlayAnimation().init(); c.getFullScreenLightRevealAnimations().forEach(it -> it.init()); c.getUnfoldTransitionWallpaperController().init(); c.getUnfoldHapticsPlayer(); c.getNaturalRotationUnfoldProgressProvider().init(); Loading
packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +48 −0 Original line number Diff line number Diff line Loading @@ -156,6 +156,54 @@ data class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevea } } data class LinearSideLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect { override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { scrim.interpolatedRevealAmount = amount scrim.startColorAlpha = getPercentPastThreshold(1 - amount, threshold = 1 - START_COLOR_REVEAL_PERCENTAGE) scrim.revealGradientEndColorAlpha = 1f - getPercentPastThreshold( amount, threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE ) val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1f, amount) if (isVertical) { scrim.setRevealGradientBounds( left = -(scrim.viewWidth) * gradientBoundsAmount, top = -(scrim.viewHeight) * gradientBoundsAmount, right = (scrim.viewWidth) * gradientBoundsAmount, bottom = (scrim.viewHeight) + (scrim.viewHeight) * gradientBoundsAmount ) } else { scrim.setRevealGradientBounds( left = -(scrim.viewWidth) * gradientBoundsAmount, top = -(scrim.viewHeight) * gradientBoundsAmount, right = (scrim.viewWidth) + (scrim.viewWidth) * gradientBoundsAmount, bottom = (scrim.viewHeight) * gradientBoundsAmount ) } } private companion object { // From which percentage we should start the gradient reveal width // E.g. if 0 - starts with 0px width, 0.6f - starts with 60% width private const val GRADIENT_START_BOUNDS_PERCENTAGE: Float = 1f // When to start changing alpha color of the gradient scrim // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely // transparent at 100% private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE: Float = 1f // When to finish displaying start color fill that reveals the content // E.g. if 0.6f - the content won't be visible at 0% and it will gradually // reduce the alpha until 60% (at this point the color fill is invisible) private const val START_COLOR_REVEAL_PERCENTAGE: Float = 1f } } data class CircleReveal( /** X-value of the circle center of the reveal. */ val centerX: Int, Loading
packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt 0 → 100644 +153 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.unfold import android.animation.ValueAnimator import android.annotation.BinderThread import android.content.Context import android.os.Handler import android.os.SystemProperties import android.util.Log import android.view.animation.DecelerateInterpolator import androidx.core.animation.addListener import com.android.internal.foldables.FoldLockSettingAvailabilityProvider import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.ScreenPowerState import com.android.systemui.statusbar.LinearSideLightRevealEffect import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation import com.android.systemui.unfold.dagger.UnfoldBg import com.android.systemui.util.animation.data.repository.AnimationStatusRepository import javax.inject.Inject import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout class FoldLightRevealOverlayAnimation @Inject constructor( private val context: Context, @UnfoldBg private val bgHandler: Handler, private val deviceStateRepository: DeviceStateRepository, private val powerInteractor: PowerInteractor, @Background private val applicationScope: CoroutineScope, private val animationStatusRepository: AnimationStatusRepository, private val controllerFactory: FullscreenLightRevealAnimationController.Factory ) : FullscreenLightRevealAnimation { private val revealProgressValueAnimator: ValueAnimator = ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT) private lateinit var controller: FullscreenLightRevealAnimationController @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null override fun init() { // This method will be called only on devices where this animation is enabled, // so normally this thread won't be created if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) { return } controller = controllerFactory.create( displaySelector = { minByOrNull { it.naturalWidth } }, effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) }, overlayContainerName = SURFACE_CONTAINER_NAME ) controller.init() applicationScope.launch(bgHandler.asCoroutineDispatcher()) { powerInteractor.screenPowerState.collect { if (it == ScreenPowerState.SCREEN_ON) { readyCallback = null } } } applicationScope.launch(bgHandler.asCoroutineDispatcher()) { deviceStateRepository.state .map { it != DeviceStateRepository.DeviceState.FOLDED } .distinctUntilChanged() .filter { isUnfolded -> isUnfolded } .collect { controller.ensureOverlayRemoved() } } applicationScope.launch(bgHandler.asCoroutineDispatcher()) { deviceStateRepository.state .filter { animationStatusRepository.areAnimationsEnabled().first() && it == DeviceStateRepository.DeviceState.FOLDED } .collect { try { withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { readyCallback = CompletableDeferred() val onReady = readyCallback?.await() readyCallback = null controller.addOverlay(ALPHA_OPAQUE, onReady) waitForScreenTurnedOn() playFoldLightRevealOverlayAnimation() } } catch (e: TimeoutCancellationException) { Log.e(TAG, "Fold light reveal animation timed out") ensureOverlayRemovedInternal() } } } } @BinderThread override fun onScreenTurningOn(onOverlayReady: Runnable) { readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run() } private suspend fun waitForScreenTurnedOn() { powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() } private fun ensureOverlayRemovedInternal() { revealProgressValueAnimator.cancel() controller.ensureOverlayRemoved() } private fun playFoldLightRevealOverlayAnimation() { revealProgressValueAnimator.duration = ANIMATION_DURATION revealProgressValueAnimator.interpolator = DecelerateInterpolator() revealProgressValueAnimator.addUpdateListener { animation -> controller.updateRevealAmount(animation.animatedFraction) } revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() }) revealProgressValueAnimator.start() } private companion object { const val TAG = "FoldLightRevealOverlayAnimation" const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L const val SURFACE_CONTAINER_NAME = "fold-overlay-container" val ANIMATION_DURATION: Long get() = SystemProperties.getLong("persist.fold_animation_duration", 200L) } }
packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt 0 → 100644 +271 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.unfold import android.content.Context import android.graphics.PixelFormat import android.hardware.display.DisplayManager import android.os.Handler import android.os.Looper import android.os.Trace import android.view.Choreographer import android.view.Display import android.view.DisplayInfo import android.view.Surface import android.view.Surface.Rotation import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.SurfaceSession import android.view.WindowManager import android.view.WindowlessWindowManager import com.android.app.tracing.traceSection import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.DisplayTracker import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.unfold.dagger.UnfoldBg import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.util.concurrency.ThreadFactory import com.android.wm.shell.displayareahelper.DisplayAreaHelper import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.lang.IllegalArgumentException import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch interface FullscreenLightRevealAnimation { fun init() fun onScreenTurningOn(onOverlayReady: Runnable) } class FullscreenLightRevealAnimationController @AssistedInject constructor( private val context: Context, private val displayManager: DisplayManager, private val threadFactory: ThreadFactory, @UnfoldBg private val bgHandler: Handler, @UnfoldBg private val rotationChangeProvider: RotationChangeProvider, private val displayAreaHelper: Optional<DisplayAreaHelper>, private val displayTracker: DisplayTracker, @Background private val applicationScope: CoroutineScope, @Main private val executor: Executor, @Assisted private val displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?, @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect, @Assisted private val overlayContainerName: String ) { private lateinit var bgExecutor: Executor private lateinit var wwm: WindowlessWindowManager private var currentRotation: Int = context.display.rotation private var root: SurfaceControlViewHost? = null private var scrimView: LightRevealScrim? = null private val rotationWatcher = RotationWatcher() private val internalDisplayInfos: Sequence<DisplayInfo> get() = displayManager .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .asSequence() .map { DisplayInfo().apply { it.getDisplayInfo(this) } } .filter { it.type == Display.TYPE_INTERNAL } var isTouchBlocked: Boolean = false set(value) { if (value != field) { traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } field = value } } fun init() { bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler) rotationChangeProvider.addCallback(rotationWatcher) buildSurface { builder -> applicationScope.launch(executor.asCoroutineDispatcher()) { val overlayContainer = builder.build() SurfaceControl.Transaction() .setLayer(overlayContainer, OVERLAY_LAYER_Z_INDEX) .show(overlayContainer) .apply() wwm = WindowlessWindowManager(context.resources.configuration, overlayContainer, null) } } } fun addOverlay( initialAlpha: Float, onOverlayReady: Runnable? = null, ) { if (!::wwm.isInitialized) { // Surface overlay is not created yet on the first SysUI launch onOverlayReady?.run() return } ensureInBackground() ensureOverlayRemoved() prepareOverlay(onOverlayReady, wwm, bgExecutor, initialAlpha) } fun ensureOverlayRemoved() { ensureInBackground() traceSection("ensureOverlayRemoved") { root?.release() root = null scrimView = null } } fun isOverlayVisible(): Boolean { return scrimView == null } fun updateRevealAmount(revealAmount: Float) { scrimView?.revealAmount = revealAmount } private fun buildSurface(onUpdated: Consumer<SurfaceControl.Builder>) { val containerBuilder = SurfaceControl.Builder(SurfaceSession()) .setContainerLayer() .setName(overlayContainerName) displayAreaHelper .get() .attachToRootDisplayArea(displayTracker.defaultDisplayId, containerBuilder, onUpdated) } private fun prepareOverlay( onOverlayReady: Runnable? = null, wwm: WindowlessWindowManager, bgExecutor: Executor, initialAlpha: Float, ) { val newRoot = SurfaceControlViewHost(context, context.display, wwm, javaClass.simpleName) val params = getLayoutParams() val newView = LightRevealScrim( context, attrs = null, initialWidth = params.width, initialHeight = params.height ) .apply { revealEffect = lightRevealEffectFactory(currentRotation) revealAmount = initialAlpha } newRoot.setView(newView, params) if (onOverlayReady != null) { Trace.beginAsyncSection("$TAG#relayout", 0) newRoot.relayout(params) { transaction -> val vsyncId = Choreographer.getSfInstance().vsyncId transaction.setFrameTimelineVsync(vsyncId).apply() transaction .setFrameTimelineVsync(vsyncId + 1) .addTransactionCommittedListener(bgExecutor) { Trace.endAsyncSection("$TAG#relayout", 0) onOverlayReady.run() } .apply() } } root = newRoot scrimView = newView } private fun ensureInBackground() { check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } } private fun getLayoutParams(): WindowManager.LayoutParams { val displayInfo = internalDisplayInfos.displaySelector() ?: throw IllegalArgumentException("No internal displays found!") return WindowManager.LayoutParams().apply { if (currentRotation.isVerticalRotation()) { height = displayInfo.naturalHeight width = displayInfo.naturalWidth } else { height = displayInfo.naturalWidth width = displayInfo.naturalHeight } format = PixelFormat.TRANSLUCENT type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY title = javaClass.simpleName layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS fitInsetsTypes = 0 flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE setTrustedOverlay() packageName = context.opPackageName } } private inner class RotationWatcher : RotationChangeProvider.RotationListener { override fun onRotationChanged(newRotation: Int) { traceSection("$TAG#onRotationChanged") { if (currentRotation != newRotation) { currentRotation = newRotation scrimView?.revealEffect = lightRevealEffectFactory(currentRotation) root?.relayout(getLayoutParams()) } } } } @AssistedFactory interface Factory { fun create( displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?, effectFactory: (rotation: Int) -> LightRevealEffect, overlayContainerName: String ): FullscreenLightRevealAnimationController } companion object { private const val TAG = "FullscreenLightRevealAnimation" private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE private const val OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 const val ALPHA_TRANSPARENT = 1f const val ALPHA_OPAQUE = 0f fun @receiver:Rotation Int.isVerticalRotation(): Boolean = this == Surface.ROTATION_0 || this == Surface.ROTATION_180 } }