Add initial unfold animation prototype under feature flag

Introduces interfaces and classes for subscribing to hinge
update events, unfold transition progress controller that
uses dynamic animations to calculate current transition
progress. Added linear light reveal scrim overlay as an
example of transition progress subscriber.

Bug: 190818044
Test: manual
Change-Id: Ibf73a8c7dfa534432e0ed3ad637f0277bc7aafb9
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 99d1516..ef8e938 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -657,6 +657,9 @@
     <!-- Indicate the display area rect for foldable devices in folded state. -->
     <string name="config_foldedArea"></string>
 
+    <!-- Indicates whether to enable an animation when unfolding a device or not -->
+    <bool name="config_unfoldTransitionEnabled">false</bool>
+
     <!-- Indicates that the device supports having more than one internal display on at the same
          time. Only applicable to devices with more than one internal display. If this option is
          set to false, DisplayManager will make additional effort to ensure no more than 1 internal
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index adb046e..908721f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3837,6 +3837,7 @@
   <java-symbol type="array" name="config_foldedDeviceStates" />
   <java-symbol type="string" name="config_foldedArea" />
   <java-symbol type="bool" name="config_supportsConcurrentInternalDisplays" />
+  <java-symbol type="bool" name="config_unfoldTransitionEnabled" />
 
   <java-symbol type="array" name="config_disableApksUnlessMatchedSku_apk_list" />
   <java-symbol type="array" name="config_disableApkUnlessMatchedSku_skus_list" />
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index b2ae2a0..3a23094 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -47,6 +47,7 @@
 
     static_libs: [
         "PluginCoreLib",
+        "androidx.dynamicanimation_dynamicanimation",
     ],
     java_version: "1.8",
     min_sdk_version: "26",
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java b/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
similarity index 100%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/CallbackController.java
rename to packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java
diff --git a/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionFactory.kt
new file mode 100644
index 0000000..7594f50
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionFactory.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+@file:JvmName("UnfoldTransitionFactory")
+
+package com.android.unfold
+
+import android.content.Context
+import android.hardware.SensorManager
+import android.hardware.devicestate.DeviceStateManager
+import android.os.Handler
+import com.android.unfold.updates.screen.ScreenStatusProvider
+import com.android.unfold.config.ANIMATION_MODE_HINGE_ANGLE
+import com.android.unfold.config.ResourceUnfoldTransitionConfig
+import com.android.unfold.config.UnfoldTransitionConfig
+import com.android.unfold.progress.FixedTimingTransitionProgressProvider
+import com.android.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
+import com.android.unfold.updates.DeviceFoldStateProvider
+import com.android.unfold.updates.hinge.EmptyHingeAngleProvider
+import com.android.unfold.updates.hinge.RotationSensorHingeAngleProvider
+import java.lang.IllegalStateException
+import java.util.concurrent.Executor
+
+fun createUnfoldTransitionProgressProvider(
+    context: Context,
+    config: UnfoldTransitionConfig,
+    screenStatusProvider: ScreenStatusProvider,
+    deviceStateManager: DeviceStateManager,
+    sensorManager: SensorManager,
+    mainHandler: Handler,
+    mainExecutor: Executor
+): UnfoldTransitionProgressProvider {
+
+    if (!config.isEnabled) {
+        throw IllegalStateException("Trying to create " +
+            "UnfoldTransitionProgressProvider when the transition is disabled")
+    }
+
+    val hingeAngleProvider =
+        if (config.mode == ANIMATION_MODE_HINGE_ANGLE) {
+            RotationSensorHingeAngleProvider(sensorManager)
+        } else {
+            EmptyHingeAngleProvider()
+        }
+
+    val foldStateProvider = DeviceFoldStateProvider(
+        context,
+        hingeAngleProvider,
+        screenStatusProvider,
+        deviceStateManager,
+        mainExecutor
+    )
+
+    return if (config.mode == ANIMATION_MODE_HINGE_ANGLE) {
+        PhysicsBasedUnfoldTransitionProgressProvider(
+            mainHandler,
+            foldStateProvider
+        )
+    } else {
+        FixedTimingTransitionProgressProvider(foldStateProvider)
+    }
+}
+
+fun createConfig(context: Context): UnfoldTransitionConfig =
+    ResourceUnfoldTransitionConfig(context)
diff --git a/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..2ddb49c
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/UnfoldTransitionProgressProvider.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.unfold
+
+import android.annotation.FloatRange
+import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.statusbar.policy.CallbackController
+
+/**
+ * Interface that allows to receive unfold transition progress updates.
+ * It can be used to update view properties based on the current animation progress.
+ * onTransitionProgress callback could be called on each frame.
+ *
+ * Use [createUnfoldTransitionProgressProvider] to create instances of this interface
+ */
+interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> {
+
+    fun destroy()
+
+    interface TransitionProgressListener {
+        fun onTransitionStarted()
+        fun onTransitionFinished()
+        fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float)
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/unfold/config/ResourceUnfoldTransitionConfig.kt b/packages/SystemUI/shared/src/com/android/unfold/config/ResourceUnfoldTransitionConfig.kt
new file mode 100644
index 0000000..bde87a5
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/config/ResourceUnfoldTransitionConfig.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.unfold.config
+
+import android.content.Context
+import android.os.SystemProperties
+
+internal class ResourceUnfoldTransitionConfig(
+    private val context: Context
+) : UnfoldTransitionConfig {
+
+    override val isEnabled: Boolean
+        get() = readIsEnabled() && mode != ANIMATION_MODE_DISABLED
+
+    @AnimationMode
+    override val mode: Int
+        get() = SystemProperties.getInt(UNFOLD_TRANSITION_MODE_PROPERTY_NAME,
+            ANIMATION_MODE_FIXED_TIMING)
+
+    private fun readIsEnabled(): Boolean = context.resources
+        .getBoolean(com.android.internal.R.bool.config_unfoldTransitionEnabled)
+}
+
+/**
+ * Temporary persistent property to control unfold transition mode
+ * See [com.android.unfold.config.AnimationMode]
+ */
+private const val UNFOLD_TRANSITION_MODE_PROPERTY_NAME = "persist.unfold.transition_mode"
diff --git a/packages/SystemUI/shared/src/com/android/unfold/config/UnfoldTransitionConfig.kt b/packages/SystemUI/shared/src/com/android/unfold/config/UnfoldTransitionConfig.kt
new file mode 100644
index 0000000..f000c69
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/config/UnfoldTransitionConfig.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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.unfold.config
+
+import android.annotation.IntDef
+
+interface UnfoldTransitionConfig {
+    val isEnabled: Boolean
+
+    @AnimationMode
+    val mode: Int
+}
+
+@IntDef(prefix = ["ANIMATION_MODE_"], value = [
+    ANIMATION_MODE_DISABLED,
+    ANIMATION_MODE_FIXED_TIMING,
+    ANIMATION_MODE_HINGE_ANGLE
+])
+
+@Retention(AnnotationRetention.SOURCE)
+annotation class AnimationMode
+
+const val ANIMATION_MODE_DISABLED = 0
+const val ANIMATION_MODE_FIXED_TIMING = 1
+const val ANIMATION_MODE_HINGE_ANGLE = 2
diff --git a/packages/SystemUI/shared/src/com/android/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/progress/FixedTimingTransitionProgressProvider.kt
new file mode 100644
index 0000000..acfe073
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/progress/FixedTimingTransitionProgressProvider.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2021 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.unfold.progress
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.util.FloatProperty
+import com.android.unfold.UnfoldTransitionProgressProvider
+import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+import com.android.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
+import com.android.unfold.updates.FoldStateProvider
+import com.android.unfold.updates.FoldStateProvider.FoldUpdate
+
+/**
+ * Emits animation progress with fixed timing after unfolding
+ */
+internal class FixedTimingTransitionProgressProvider(
+    private val foldStateProvider: FoldStateProvider
+) : UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
+
+    private val animatorListener = AnimatorListener()
+    private val animator =
+        ObjectAnimator.ofFloat(this, AnimationProgressProperty, 0f, 1f)
+            .apply {
+                duration = TRANSITION_TIME_MILLIS
+                addListener(animatorListener)
+            }
+
+
+    private var transitionProgress: Float = 0.0f
+        set(value) {
+            listeners.forEach { it.onTransitionProgress(value) }
+            field = value
+        }
+
+    private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+    init {
+        foldStateProvider.addCallback(this)
+        foldStateProvider.start()
+    }
+
+    override fun destroy() {
+        animator.cancel()
+        foldStateProvider.removeCallback(this)
+        foldStateProvider.stop()
+    }
+
+    override fun onFoldUpdate(@FoldUpdate update: Int) {
+        when (update) {
+            FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE ->
+                animator.start()
+            FOLD_UPDATE_FINISH_CLOSED ->
+                animator.cancel()
+        }
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners.remove(listener)
+    }
+
+    override fun onHingeAngleUpdate(angle: Float) {
+    }
+
+    private object AnimationProgressProperty :
+        FloatProperty<FixedTimingTransitionProgressProvider>("animation_progress") {
+
+        override fun setValue(
+            provider: FixedTimingTransitionProgressProvider,
+            value: Float
+        ) {
+            provider.transitionProgress = value
+        }
+
+        override fun get(provider: FixedTimingTransitionProgressProvider): Float =
+            provider.transitionProgress
+    }
+
+    private inner class AnimatorListener : Animator.AnimatorListener {
+
+        override fun onAnimationStart(animator: Animator) {
+            listeners.forEach { it.onTransitionStarted() }
+        }
+
+        override fun onAnimationEnd(animator: Animator) {
+            listeners.forEach { it.onTransitionFinished() }
+        }
+
+        override fun onAnimationRepeat(animator: Animator) {
+        }
+
+        override fun onAnimationCancel(animator: Animator) {
+        }
+    }
+
+    private companion object {
+        private const val TRANSITION_TIME_MILLIS = 400L
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..d9d037f
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2021 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.unfold.progress
+
+import android.os.Handler
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.unfold.UnfoldTransitionProgressProvider
+import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
+import com.android.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
+import com.android.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
+import com.android.unfold.updates.FoldStateProvider
+import com.android.unfold.updates.FoldStateProvider.FoldUpdate
+import com.android.unfold.updates.FoldStateProvider.FoldUpdatesListener
+
+/**
+ * Maps fold updates to unfold transition progress using DynamicAnimation.
+ *
+ * TODO(b/193793338) Current limitations:
+ *  - doesn't handle folding transition
+ *  - doesn't handle postures
+ */
+internal class PhysicsBasedUnfoldTransitionProgressProvider(
+    private val handler: Handler,
+    private val foldStateProvider: FoldStateProvider
+) :
+    UnfoldTransitionProgressProvider,
+    FoldUpdatesListener,
+    DynamicAnimation.OnAnimationEndListener {
+
+    private val springAnimation = SpringAnimation(this, AnimationProgressProperty)
+        .apply {
+            addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider)
+        }
+
+    private val timeoutRunnable = TimeoutRunnable()
+
+    private var isTransitionRunning = false
+    private var isAnimatedCancelRunning = false
+
+    private var transitionProgress: Float = 0.0f
+        set(value) {
+            if (isTransitionRunning) {
+                listeners.forEach { it.onTransitionProgress(value) }
+            }
+            field = value
+        }
+
+    private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
+
+    init {
+        foldStateProvider.addCallback(this)
+        foldStateProvider.start()
+    }
+
+    override fun destroy() {
+        foldStateProvider.stop()
+    }
+
+    override fun onHingeAngleUpdate(angle: Float) {
+        if (!isTransitionRunning || isAnimatedCancelRunning) return
+        springAnimation.animateToFinalPosition(angle / 180f)
+    }
+
+    override fun onFoldUpdate(@FoldUpdate update: Int) {
+        when (update) {
+            FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {
+                onStartTransition()
+                startTransition(startValue = 0f)
+            }
+            FOLD_UPDATE_FINISH_FULL_OPEN -> {
+                cancelTransition(endValue = 1f, animate = true)
+            }
+            FOLD_UPDATE_FINISH_CLOSED -> {
+                cancelTransition(endValue = 0f, animate = false)
+            }
+        }
+    }
+
+    private fun cancelTransition(endValue: Float, animate: Boolean) {
+        handler.removeCallbacks(timeoutRunnable)
+
+        if (animate) {
+            isAnimatedCancelRunning = true
+            springAnimation.animateToFinalPosition(endValue)
+        } else {
+            transitionProgress = endValue
+            isAnimatedCancelRunning = false
+            isTransitionRunning = false
+            springAnimation.cancel()
+
+            listeners.forEach {
+                it.onTransitionFinished()
+            }
+        }
+    }
+
+    override fun onAnimationEnd(
+        animation: DynamicAnimation<out DynamicAnimation<*>>,
+        canceled: Boolean,
+        value: Float,
+        velocity: Float
+    ) {
+        if (isAnimatedCancelRunning) {
+            cancelTransition(value, animate = false)
+        }
+    }
+
+    private fun onStartTransition() {
+        listeners.forEach {
+            it.onTransitionStarted()
+        }
+        isTransitionRunning = true
+    }
+
+    private fun startTransition(startValue: Float) {
+        if (!isTransitionRunning) onStartTransition()
+
+        springAnimation.apply {
+            spring = SpringForce().apply {
+                finalPosition = startValue
+                dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
+                stiffness = SPRING_STIFFNESS
+            }
+            minimumVisibleChange = MINIMAL_VISIBLE_CHANGE
+            setStartValue(startValue)
+            setMinValue(0f)
+            setMaxValue(1f)
+        }
+
+        springAnimation.start()
+
+        handler.postDelayed(timeoutRunnable, TRANSITION_TIMEOUT_MILLIS)
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners.remove(listener)
+    }
+
+    private inner class TimeoutRunnable : Runnable {
+
+        override fun run() {
+            cancelTransition(endValue = 1f, animate = true)
+        }
+    }
+
+    private object AnimationProgressProperty :
+        FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") {
+
+        override fun setValue(
+            provider: PhysicsBasedUnfoldTransitionProgressProvider,
+            value: Float
+        ) {
+            provider.transitionProgress = value
+        }
+
+        override fun getValue(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
+            provider.transitionProgress
+    }
+}
+
+private const val TRANSITION_TIMEOUT_MILLIS = 2000L
+private const val SPRING_STIFFNESS = 200.0f
+private const val MINIMAL_VISIBLE_CHANGE = 0.001f
diff --git a/packages/SystemUI/shared/src/com/android/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/updates/DeviceFoldStateProvider.kt
new file mode 100644
index 0000000..3a21b80
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/updates/DeviceFoldStateProvider.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 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.unfold.updates
+
+import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import androidx.core.util.Consumer
+import com.android.unfold.updates.screen.ScreenStatusProvider
+import com.android.unfold.updates.FoldStateProvider.FoldUpdate
+import com.android.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.unfold.updates.hinge.FULLY_OPEN_DEGREES
+import com.android.unfold.updates.hinge.HingeAngleProvider
+import java.util.concurrent.Executor
+
+internal class DeviceFoldStateProvider(
+    context: Context,
+    private val hingeAngleProvider: HingeAngleProvider,
+    private val screenStatusProvider: ScreenStatusProvider,
+    private val deviceStateManager: DeviceStateManager,
+    private val mainExecutor: Executor
+) : FoldStateProvider {
+
+    private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
+
+    @FoldUpdate
+    private var lastFoldUpdate: Int? = null
+
+    private val hingeAngleListener = HingeAngleListener()
+    private val screenListener = ScreenStatusListener()
+    private val foldStateListener = FoldStateListener(context)
+
+    private var isFolded = false
+
+    override fun start() {
+        deviceStateManager.registerCallback(
+            mainExecutor,
+            foldStateListener
+        )
+        screenStatusProvider.addCallback(screenListener)
+        hingeAngleProvider.addCallback(hingeAngleListener)
+    }
+
+    override fun stop() {
+        screenStatusProvider.removeCallback(screenListener)
+        deviceStateManager.unregisterCallback(foldStateListener)
+        hingeAngleProvider.removeCallback(hingeAngleListener)
+        hingeAngleProvider.stop()
+    }
+
+    override fun addCallback(listener: FoldUpdatesListener) {
+        outputListeners.add(listener)
+    }
+
+    override fun removeCallback(listener: FoldUpdatesListener) {
+        outputListeners.remove(listener)
+    }
+
+    private fun onHingeAngle(angle: Float) {
+        when (lastFoldUpdate) {
+            FOLD_UPDATE_FINISH_FULL_OPEN -> {
+                if (FULLY_OPEN_DEGREES - angle > MOVEMENT_THRESHOLD_DEGREES) {
+                    lastFoldUpdate = FOLD_UPDATE_START_CLOSING
+                    outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_CLOSING) }
+                }
+            }
+            FOLD_UPDATE_START_OPENING, FOLD_UPDATE_START_CLOSING -> {
+                if (FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES) {
+                    lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN
+                    outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) }
+                }
+            }
+        }
+
+        outputListeners.forEach { it.onHingeAngleUpdate(angle) }
+    }
+
+    private inner class FoldStateListener(context: Context) :
+        DeviceStateManager.FoldStateListener(context, { folded: Boolean ->
+            isFolded = folded
+
+            if (folded) {
+                lastFoldUpdate = FOLD_UPDATE_FINISH_CLOSED
+                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) }
+                hingeAngleProvider.stop()
+            } else {
+                lastFoldUpdate = FOLD_UPDATE_START_OPENING
+                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_OPENING) }
+                hingeAngleProvider.start()
+            }
+        })
+
+    private inner class ScreenStatusListener :
+        ScreenStatusProvider.ScreenListener {
+
+        override fun onScreenTurnedOn() {
+            if (!isFolded) {
+                outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) }
+            }
+        }
+    }
+
+    private inner class HingeAngleListener : Consumer<Float> {
+
+        override fun accept(angle: Float) {
+            onHingeAngle(angle)
+        }
+    }
+}
+
+private const val MOVEMENT_THRESHOLD_DEGREES = 10f
+private const val FULLY_OPEN_THRESHOLD_DEGREES = 10f
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/unfold/updates/FoldStateProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/updates/FoldStateProvider.kt
new file mode 100644
index 0000000..2c3a6ec
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/updates/FoldStateProvider.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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.unfold.updates
+
+import android.annotation.FloatRange
+import android.annotation.IntDef
+import com.android.unfold.updates.FoldStateProvider.FoldUpdatesListener
+import com.android.systemui.statusbar.policy.CallbackController
+
+/**
+ * Allows to subscribe to main events related to fold/unfold process such as hinge angle update,
+ * start folding/unfolding, screen availability
+ */
+internal interface FoldStateProvider : CallbackController<FoldUpdatesListener> {
+    fun start()
+    fun stop()
+
+    interface FoldUpdatesListener {
+        fun onHingeAngleUpdate(@FloatRange(from = 0.0, to = 180.0) angle: Float)
+        fun onFoldUpdate(@FoldUpdate update: Int)
+    }
+
+    @IntDef(prefix = ["FOLD_UPDATE_"], value = [
+        FOLD_UPDATE_START_OPENING,
+        FOLD_UPDATE_HALF_OPEN,
+        FOLD_UPDATE_START_CLOSING,
+        FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE,
+        FOLD_UPDATE_FINISH_HALF_OPEN,
+        FOLD_UPDATE_FINISH_FULL_OPEN,
+        FOLD_UPDATE_FINISH_CLOSED
+    ])
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class FoldUpdate
+}
+
+const val FOLD_UPDATE_START_OPENING = 0
+const val FOLD_UPDATE_HALF_OPEN = 1
+const val FOLD_UPDATE_START_CLOSING = 2
+const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 3
+const val FOLD_UPDATE_FINISH_HALF_OPEN = 4
+const val FOLD_UPDATE_FINISH_FULL_OPEN = 5
+const val FOLD_UPDATE_FINISH_CLOSED = 6
diff --git a/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/EmptyHingeAngleProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/EmptyHingeAngleProvider.kt
new file mode 100644
index 0000000..905b086
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/EmptyHingeAngleProvider.kt
@@ -0,0 +1,17 @@
+package com.android.unfold.updates.hinge
+
+import androidx.core.util.Consumer
+
+internal class EmptyHingeAngleProvider : HingeAngleProvider {
+    override fun start() {
+    }
+
+    override fun stop() {
+    }
+
+    override fun removeCallback(listener: Consumer<Float>) {
+    }
+
+    override fun addCallback(listener: Consumer<Float>) {
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/HingeAngleProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/HingeAngleProvider.kt
new file mode 100644
index 0000000..4196f60
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/HingeAngleProvider.kt
@@ -0,0 +1,12 @@
+package com.android.unfold.updates.hinge
+
+import androidx.core.util.Consumer
+import com.android.systemui.statusbar.policy.CallbackController
+
+internal interface HingeAngleProvider : CallbackController<Consumer<Float>> {
+    fun start()
+    fun stop()
+}
+
+const val FULLY_OPEN_DEGREES = 180f
+const val FULLY_CLOSED_DEGREES = 0f
diff --git a/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/RotationSensorHingeAngleProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/RotationSensorHingeAngleProvider.kt
new file mode 100644
index 0000000..011582e
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/updates/hinge/RotationSensorHingeAngleProvider.kt
@@ -0,0 +1,67 @@
+package com.android.unfold.updates.hinge
+
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import androidx.core.util.Consumer
+import com.android.systemui.shared.recents.utilities.Utilities
+
+/**
+ * Temporary hinge angle provider that uses rotation sensor instead.
+ * It requires to have the device in a certain position to work correctly
+ * (flat to the ground)
+ */
+internal class RotationSensorHingeAngleProvider(
+    private val sensorManager: SensorManager
+) : HingeAngleProvider {
+
+    private val sensorListener = HingeAngleSensorListener()
+    private val listeners: MutableList<Consumer<Float>> = arrayListOf()
+
+    override fun start() {
+        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
+        sensorManager.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_FASTEST)
+    }
+
+    override fun stop() {
+        sensorManager.unregisterListener(sensorListener)
+    }
+
+    override fun removeCallback(listener: Consumer<Float>) {
+        listeners.remove(listener)
+    }
+
+    override fun addCallback(listener: Consumer<Float>) {
+        listeners.add(listener)
+    }
+
+    private fun onHingeAngle(angle: Float) {
+        listeners.forEach { it.accept(angle) }
+    }
+
+    private inner class HingeAngleSensorListener : SensorEventListener {
+
+        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
+        }
+
+        override fun onSensorChanged(event: SensorEvent) {
+            // Jumbojack sends incorrect sensor reading 1.0f event in the beginning, let's ignore it
+            if (event.values[3] == 1.0f) return
+
+            val angleRadians = event.values.convertToAngle()
+            val hingeAngleDegrees = Math.toDegrees(angleRadians).toFloat()
+            val angle = Utilities.clamp(hingeAngleDegrees, FULLY_CLOSED_DEGREES, FULLY_OPEN_DEGREES)
+            onHingeAngle(angle)
+        }
+
+        private val rotationMatrix = FloatArray(9)
+        private val resultOrientation = FloatArray(9)
+
+        private fun FloatArray.convertToAngle(): Double {
+            SensorManager.getRotationMatrixFromVector(rotationMatrix, this)
+            SensorManager.getOrientation(rotationMatrix, resultOrientation)
+            return resultOrientation[2] + Math.PI
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/unfold/updates/screen/ScreenStatusProvider.kt b/packages/SystemUI/shared/src/com/android/unfold/updates/screen/ScreenStatusProvider.kt
new file mode 100644
index 0000000..a65e888
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/unfold/updates/screen/ScreenStatusProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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.unfold.updates.screen
+
+import com.android.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import com.android.systemui.statusbar.policy.CallbackController
+
+interface ScreenStatusProvider : CallbackController<ScreenListener> {
+
+    interface ScreenListener {
+        /**
+         * Called when the screen is on and ready (windows are drawn and screen blocker is removed)
+         */
+        fun onScreenTurnedOn()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
index a94df9e..2d200e3 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java
@@ -24,6 +24,8 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.om.OverlayManager;
+import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.hardware.display.ColorDisplayManager;
 import android.os.Handler;
@@ -59,6 +61,7 @@
 import com.android.systemui.doze.AlwaysOnDisplayPolicy;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.LifecycleScreenStatusProvider;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBarA11yHelper;
 import com.android.systemui.navigationbar.NavigationBarController;
@@ -77,6 +80,9 @@
 import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.unfold.UnfoldTransitionFactory;
+import com.android.unfold.UnfoldTransitionProgressProvider;
+import com.android.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -378,6 +384,37 @@
     /** */
     @Provides
     @SysUISingleton
+    public UnfoldTransitionProgressProvider provideUnfoldTransitionProgressProvider(
+            Context context,
+            UnfoldTransitionConfig config,
+            LifecycleScreenStatusProvider screenStatusProvider,
+            DeviceStateManager deviceStateManager,
+            SensorManager sensorManager,
+            @Main Executor executor,
+            @Main Handler handler
+    ) {
+        return UnfoldTransitionFactory
+                .createUnfoldTransitionProgressProvider(
+                        context,
+                        config,
+                        screenStatusProvider,
+                        deviceStateManager,
+                        sensorManager,
+                        handler,
+                        executor
+                );
+    }
+
+    /** */
+    @Provides
+    @SysUISingleton
+    public UnfoldTransitionConfig provideUnfoldTransitionConfig(Context context) {
+        return UnfoldTransitionFactory.createConfig(context);
+    }
+
+    /** */
+    @Provides
+    @SysUISingleton
     public Choreographer providesChoreographer() {
         return Choreographer.getInstance();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 954ba79..4d1608f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -41,6 +41,7 @@
 import android.content.res.Resources;
 import android.hardware.SensorManager;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.ColorDisplayManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.face.FaceManager;
@@ -159,6 +160,12 @@
 
     @Provides
     @Singleton
+    static DeviceStateManager provideDeviceStateManager(Context context) {
+        return context.getSystemService(DeviceStateManager.class);
+    }
+
+    @Provides
+    @Singleton
     static IActivityManager provideIActivityManager() {
         return ActivityManager.getService();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
new file mode 100644
index 0000000..d8905a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.keyguard
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.unfold.updates.screen.ScreenStatusProvider
+import com.android.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import javax.inject.Inject
+
+@SysUISingleton
+class LifecycleScreenStatusProvider @Inject constructor(screenLifecycle: ScreenLifecycle) :
+    ScreenStatusProvider, ScreenLifecycle.Observer {
+
+    init {
+        screenLifecycle.addObserver(this)
+    }
+
+    private val listeners: MutableList<ScreenListener> = mutableListOf()
+
+    override fun removeCallback(listener: ScreenListener) {
+        listeners.remove(listener)
+    }
+
+    override fun addCallback(listener: ScreenListener) {
+        listeners.add(listener)
+    }
+
+    override fun onScreenTurnedOn() {
+        listeners.forEach(ScreenListener::onScreenTurnedOn)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 538db61..8780ad8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -83,6 +83,35 @@
     }
 }
 
+class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
+
+    private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE
+
+    override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
+        val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
+
+        // TODO(b/193801466): add alpha reveal in the beginning as well
+        scrim.revealGradientEndColorAlpha =
+            1f - LightRevealEffect.getPercentPastThreshold(interpolatedAmount, threshold = 0.6f)
+
+        if (isVertical) {
+            scrim.setRevealGradientBounds(
+                left = scrim.width / 2 - (scrim.width / 2) * interpolatedAmount,
+                top = 0f,
+                right = scrim.width / 2 + (scrim.width / 2) * interpolatedAmount,
+                bottom = scrim.height.toFloat()
+            )
+        } else {
+            scrim.setRevealGradientBounds(
+                left = 0f,
+                top = scrim.height / 2 - (scrim.height / 2) * interpolatedAmount,
+                right = scrim.width.toFloat(),
+                bottom = scrim.height / 2 + (scrim.height / 2) * interpolatedAmount
+            )
+        }
+    }
+}
+
 class CircleReveal(
     /** X-value of the circle center of the reveal. */
     val centerX: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index bc3a770..e0bdd68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -187,6 +187,7 @@
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.settings.brightness.BrightnessSlider;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.BackDropView;
 import com.android.systemui.statusbar.CircleReveal;
@@ -245,6 +246,7 @@
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -467,6 +469,8 @@
     protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     private final BrightnessSlider.Factory mBrightnessSliderFactory;
     private final FeatureFlags mFeatureFlags;
+    private final UnfoldTransitionConfig mUnfoldTransitionConfig;
+    private final Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimation;
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
@@ -801,6 +805,8 @@
             NotificationIconAreaController notificationIconAreaController,
             BrightnessSlider.Factory brightnessSliderFactory,
             WiredChargingRippleController chargingRippleAnimationController,
+            UnfoldTransitionConfig unfoldTransitionConfig,
+            Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
             OngoingCallController ongoingCallController,
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
@@ -887,6 +893,8 @@
         mNotificationIconAreaController = notificationIconAreaController;
         mBrightnessSliderFactory = brightnessSliderFactory;
         mChargingRippleAnimationController = chargingRippleAnimationController;
+        mUnfoldTransitionConfig = unfoldTransitionConfig;
+        mUnfoldLightRevealOverlayAnimation = unfoldLightRevealOverlayAnimation;
         mOngoingCallController = ongoingCallController;
         mAnimationScheduler = animationScheduler;
         mStatusBarLocationPublisher = locationPublisher;
@@ -1058,6 +1066,10 @@
 
         mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
 
+        if (mUnfoldTransitionConfig.isEnabled()) {
+            mUnfoldLightRevealOverlayAnimation.get().init();
+        }
+
         mPluginManager.addPluginListener(
                 new PluginListener<OverlayPlugin>() {
                     private ArraySet<OverlayPlugin> mOverlays = new ArraySet<>();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index 716d1db..3dc24e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -48,6 +48,7 @@
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.settings.brightness.BrightnessSlider;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -105,6 +106,7 @@
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.wmshell.BubblesManager;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -213,6 +215,8 @@
             NotificationIconAreaController notificationIconAreaController,
             BrightnessSlider.Factory brightnessSliderFactory,
             WiredChargingRippleController chargingRippleAnimationController,
+            UnfoldTransitionConfig unfoldTransitionConfig,
+            Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation,
             OngoingCallController ongoingCallController,
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
@@ -302,6 +306,8 @@
                 notificationIconAreaController,
                 brightnessSliderFactory,
                 chargingRippleAnimationController,
+                unfoldTransitionConfig,
+                unfoldLightRevealOverlayAnimation,
                 ongoingCallController,
                 animationScheduler,
                 locationPublisher,
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
new file mode 100644
index 0000000..8e7c49a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2021 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.devicestate.DeviceStateManager
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener
+import android.view.Surface
+import android.view.WindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.unfold.UnfoldTransitionProgressProvider
+import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.LinearLightRevealEffect
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import javax.inject.Inject
+
+@SysUISingleton
+class UnfoldLightRevealOverlayAnimation @Inject constructor(
+    private val context: Context,
+    private val deviceStateManager: DeviceStateManager,
+    private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+    @Main private val executor: Executor,
+    private val windowManager: WindowManager
+) {
+
+    private val transitionListener = TransitionListener()
+    private var scrimView: LightRevealScrim? = null
+
+    fun init() {
+        deviceStateManager.registerCallback(executor, FoldListener())
+        unfoldTransitionProgressProvider.addCallback(transitionListener)
+    }
+
+    private inner class TransitionListener : TransitionProgressListener {
+
+        override fun onTransitionProgress(progress: Float) {
+            scrimView?.revealAmount = progress
+        }
+
+        override fun onTransitionFinished() {
+            removeOverlayView()
+        }
+
+        override fun onTransitionStarted() {
+        }
+    }
+
+    private inner class FoldListener : FoldStateListener(context, Consumer { isFolded ->
+        if (isFolded) {
+            removeOverlayView()
+        } else {
+            // Add overlay view before starting the transition as soon as we unfolded the device
+            addOverlayView()
+        }
+    })
+
+    private fun addOverlayView() {
+        val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
+        params.height = WindowManager.LayoutParams.MATCH_PARENT
+        params.width = WindowManager.LayoutParams.MATCH_PARENT
+        params.format = PixelFormat.TRANSLUCENT
+
+        // TODO(b/193801466): create a separate type for this overlay
+        params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
+        params.title = "Unfold Light Reveal Animation"
+        params.layoutInDisplayCutoutMode =
+            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        params.fitInsetsTypes = 0
+        params.flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+            or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
+        params.setTrustedOverlay()
+
+        val rotation = windowManager.defaultDisplay.rotation
+        val isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+
+        val newScrimView = LightRevealScrim(context, null)
+            .apply {
+                revealEffect = LinearLightRevealEffect(isVerticalFold)
+                revealAmountListener = Consumer {}
+                revealAmount = 0f
+            }
+
+        val packageName: String = newScrimView.context.opPackageName
+        params.packageName = packageName
+        params.hideTimeoutMilliseconds = OVERLAY_HIDE_TIMEOUT_MILLIS
+
+        if (scrimView?.parent != null) {
+            windowManager.removeView(scrimView)
+        }
+
+        this.scrimView = newScrimView
+
+        try {
+            windowManager.addView(scrimView, params)
+        } catch (e: WindowManager.BadTokenException) {
+            e.printStackTrace()
+        }
+    }
+
+    private fun removeOverlayView() {
+        scrimView?.let {
+            if (it.parent != null) {
+                windowManager.removeViewImmediate(it)
+            }
+            scrimView = null
+        }
+    }
+}
+
+private const val OVERLAY_HIDE_TIMEOUT_MILLIS = 10_000L
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index c504fd8..b74d7cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -98,6 +98,7 @@
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.settings.brightness.BrightnessSlider;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.unfold.config.UnfoldTransitionConfig;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -146,6 +147,7 @@
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.volume.VolumeComponent;
@@ -266,6 +268,8 @@
     @Mock private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
     @Mock private BrightnessSlider.Factory mBrightnessSliderFactory;
     @Mock private WiredChargingRippleController mWiredChargingRippleController;
+    @Mock private UnfoldTransitionConfig mUnfoldTransitionConfig;
+    @Mock private Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimationLazy;
     @Mock private OngoingCallController mOngoingCallController;
     @Mock private SystemStatusAnimationScheduler mAnimationScheduler;
     @Mock private StatusBarLocationPublisher mLocationPublisher;
@@ -439,6 +443,8 @@
                 mNotificationIconAreaController,
                 mBrightnessSliderFactory,
                 mWiredChargingRippleController,
+                mUnfoldTransitionConfig,
+                mUnfoldLightRevealOverlayAnimationLazy,
                 mOngoingCallController,
                 mAnimationScheduler,
                 mLocationPublisher,