Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c171ffe6 authored by Nick Chameyev's avatar Nick Chameyev Committed by Android (Google) Code Review
Browse files

Merge "Animation on fold for fold lock behavior setting" into main

parents 08dd4252 1aa51f54
Loading
Loading
Loading
Loading
+19 −15
Original line number Diff line number Diff line
@@ -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) {
@@ -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()) {
@@ -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() {
+1 −1
Original line number Diff line number Diff line
@@ -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();
+48 −0
Original line number Diff line number Diff line
@@ -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,
+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)
    }
}
+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