Merge "[Unfold animation] Start Launcher animation preemptively to synchronize the first frame" into udc-dev
diff --git a/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java b/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
index ad11b7e..328a727 100644
--- a/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/BaseUnfoldMoveFromCenterAnimator.java
@@ -43,6 +43,10 @@
             new UnfoldMoveFromCenterRotationListener();
     private boolean mAnimationInProgress = false;
 
+    // Save the last transition progress so we can re-apply it in case we re-register the view for
+    // the animation (by calling onPrepareViewsForAnimation)
+    private Float mLastTransitionProgress = null;
+
     public BaseUnfoldMoveFromCenterAnimator(WindowManager windowManager,
             RotationChangeProvider rotationChangeProvider) {
         mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
@@ -63,11 +67,13 @@
     @Override
     public void onTransitionProgress(float progress) {
         mMoveFromCenterAnimation.onTransitionProgress(progress);
+        mLastTransitionProgress = progress;
     }
 
     @CallSuper
     @Override
     public void onTransitionFinished() {
+        mLastTransitionProgress = null;
         mAnimationInProgress = false;
         mRotationChangeProvider.removeCallback(mRotationListener);
         mMoveFromCenterAnimation.onTransitionFinished();
@@ -93,8 +99,11 @@
         mOriginalClipToPadding.clear();
     }
 
+    @CallSuper
     protected void onPrepareViewsForAnimation() {
-
+        if (mLastTransitionProgress != null) {
+            mMoveFromCenterAnimation.onTransitionProgress(mLastTransitionProgress);
+        }
     }
 
     protected void registerViewForAnimation(View view) {
diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
index 8fdafc6..6d15e8b 100644
--- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
+++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
@@ -27,10 +27,15 @@
 
 import androidx.core.view.OneShotPreDrawListener;
 
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.HorizontalInsettableView;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.unfold.PreemptiveUnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
@@ -40,7 +45,7 @@
 /**
  * Controls animations that are happening during unfolding foldable devices
  */
-public class LauncherUnfoldAnimationController {
+public class LauncherUnfoldAnimationController implements OnDeviceProfileChangeListener {
 
     // Percentage of the width of the quick search bar that will be reduced
     // from the both sides of the bar when progress is 0
@@ -55,9 +60,11 @@
     private final NaturalRotationUnfoldProgressProvider mNaturalOrientationProgressProvider;
     private final UnfoldMoveFromCenterHotseatAnimator mUnfoldMoveFromCenterHotseatAnimator;
     private final UnfoldMoveFromCenterWorkspaceAnimator mUnfoldMoveFromCenterWorkspaceAnimator;
+    private PreemptiveUnfoldTransitionProgressProvider mPreemptiveProgressProvider = null;
+    private Boolean mIsTablet = null;
 
     private static final String TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION =
-            "waitingOneFrameBeforeHandlingUnfoldAnimation";
+            "LauncherUnfoldAnimationController#waitingForTheNextFrame";
 
     @Nullable
     private HorizontalInsettableView mQsbInsettable;
@@ -68,8 +75,19 @@
             UnfoldTransitionProgressProvider unfoldTransitionProgressProvider,
             RotationChangeProvider rotationChangeProvider) {
         mLauncher = launcher;
-        mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
-                unfoldTransitionProgressProvider);
+
+        if (FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
+            mPreemptiveProgressProvider = new PreemptiveUnfoldTransitionProgressProvider(
+                    unfoldTransitionProgressProvider, launcher.getMainThreadHandler());
+            mPreemptiveProgressProvider.init();
+
+            mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
+                    mPreemptiveProgressProvider);
+        } else {
+            mProgressProvider = new ScopedUnfoldTransitionProgressProvider(
+                    unfoldTransitionProgressProvider);
+        }
+
         mUnfoldMoveFromCenterHotseatAnimator = new UnfoldMoveFromCenterHotseatAnimator(launcher,
                 windowManager, rotationChangeProvider);
         mUnfoldMoveFromCenterWorkspaceAnimator = new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
@@ -85,6 +103,8 @@
         // Animated only in natural orientation
         mNaturalOrientationProgressProvider.addCallback(new QsbAnimationListener());
         mNaturalOrientationProgressProvider.addCallback(mUnfoldMoveFromCenterHotseatAnimator);
+
+        mLauncher.addOnDeviceProfileChangeListener(this);
     }
 
     /**
@@ -96,17 +116,21 @@
             mQsbInsettable = (HorizontalInsettableView) hotseat.getQsb();
         }
 
-        handleTransitionOnNextFrame();
+        mProgressProvider.setReadyToHandleTransition(true);
     }
 
-    private void handleTransitionOnNextFrame() {
+    private void preemptivelyStartAnimationOnNextFrame() {
         Trace.asyncTraceBegin(Trace.TRACE_TAG_APP,
                 TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0);
+
+        // Start the animation (and apply the transformations) in pre-draw listener to make sure
+        // that the views are laid out as some transformations depend on the view sizes and position
         OneShotPreDrawListener.add(mLauncher.getWorkspace(),
                 () -> {
                     Trace.asyncTraceEnd(Trace.TRACE_TAG_APP,
                             TRACE_WAIT_TO_HANDLE_UNFOLD_TRANSITION, /* cookie= */ 0);
-                    mProgressProvider.setReadyToHandleTransition(true);
+                    mPreemptiveProgressProvider.preemptivelyStartTransition(
+                            /* initialProgress= */ 0f);
                 });
     }
 
@@ -124,14 +148,34 @@
     public void onDestroy() {
         mProgressProvider.destroy();
         mNaturalOrientationProgressProvider.destroy();
+        mLauncher.removeOnDeviceProfileChangeListener(this);
     }
 
-    /** Called when launcher finished binding its items. */
+    /**
+     * Called when launcher has finished binding its items
+     */
     public void updateRegisteredViewsIfNeeded() {
         mUnfoldMoveFromCenterHotseatAnimator.updateRegisteredViewsIfNeeded();
         mUnfoldMoveFromCenterWorkspaceAnimator.updateRegisteredViewsIfNeeded();
     }
 
+    @Override
+    public void onDeviceProfileChanged(DeviceProfile dp) {
+        if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
+            return;
+        }
+
+        if (mIsTablet != null && dp.isTablet != mIsTablet) {
+            if (dp.isTablet && SystemUiProxy.INSTANCE.get(mLauncher).isActive()) {
+                // Preemptively start the unfold animation to make sure that we have drawn
+                // the first frame of the animation before the screen gets unblocked
+                preemptivelyStartAnimationOnNextFrame();
+            }
+        }
+
+        mIsTablet = dp.isTablet;
+    }
+
     private class QsbAnimationListener implements TransitionProgressListener {
 
         @Override
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
index 70a12d6..c8141b4 100644
--- a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterHotseatAnimator.java
@@ -48,6 +48,8 @@
             View child = hotseatIcons.getChildAt(i);
             registerViewForAnimation(child);
         }
+
+        super.onPrepareViewsForAnimation();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
index 7da103e..c05b38f 100644
--- a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
@@ -58,6 +58,8 @@
 
         setClipChildren(workspace, false);
         setClipToPadding(workspace, true);
+
+        super.onPrepareViewsForAnimation();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/unfold/PreemptiveUnfoldTransitionProgressProvider.kt b/quickstep/src/com/android/quickstep/util/unfold/PreemptiveUnfoldTransitionProgressProvider.kt
new file mode 100644
index 0000000..a9cd048
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/PreemptiveUnfoldTransitionProgressProvider.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util.unfold
+
+import android.os.Handler
+import android.os.Trace
+import android.util.Log
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+
+/**
+ * Transition progress provider wrapper that can preemptively start the transition on demand
+ * without relying on the source provider. When the source provider has started the animation
+ * it switches to it.
+ *
+ * This might be useful when we want to synchronously start the unfold animation and render
+ * the first frame during turning on the screen. For example, this is used in Launcher where
+ * we need to render the first frame of the animation immediately after receiving a configuration
+ * change event so Window Manager will wait for this frame to be rendered before unblocking
+ * the screen. We can't rely on the original transition progress as it starts the animation
+ * after the screen fully turned on (and unblocked), at this moment it is already too late to
+ * start the animation.
+ *
+ * Using this provider we could render the first frame preemptively by sending 'transition started'
+ * and '0' transition progress before the original progress provider sends these events.
+ */
+class PreemptiveUnfoldTransitionProgressProvider(
+        private val source: UnfoldTransitionProgressProvider,
+        private val handler: Handler
+) : UnfoldTransitionProgressProvider, TransitionProgressListener {
+
+    private val timeoutRunnable = Runnable {
+        if (isRunning) {
+            listeners.forEach { it.onTransitionFinished() }
+            onPreemptiveStartFinished()
+            Log.wtf(TAG, "Timeout occurred when waiting for the source transition to start")
+        }
+    }
+
+    private val listeners = arrayListOf<TransitionProgressListener>()
+    private var isPreemptivelyRunning = false
+    private var isSourceRunning = false
+
+    private val isRunning: Boolean
+        get() = isPreemptivelyRunning || isSourceRunning
+
+    private val sourceListener =
+            object : TransitionProgressListener {
+                override fun onTransitionStarted() {
+                    handler.removeCallbacks(timeoutRunnable)
+
+                    if (!isRunning) {
+                        listeners.forEach { it.onTransitionStarted() }
+                    }
+
+                    onPreemptiveStartFinished()
+                    isSourceRunning = true
+                }
+
+                override fun onTransitionProgress(progress: Float) {
+                    if (isRunning) {
+                        listeners.forEach { it.onTransitionProgress(progress) }
+                        isSourceRunning = true
+                    }
+                }
+
+                override fun onTransitionFinishing() {
+                    if (isRunning) {
+                        listeners.forEach { it.onTransitionFinishing() }
+                        isSourceRunning = true
+                    }
+                }
+
+                override fun onTransitionFinished() {
+                    if (isRunning) {
+                        listeners.forEach { it.onTransitionFinished() }
+                    }
+
+                    isSourceRunning = false
+                    onPreemptiveStartFinished()
+                    handler.removeCallbacks(timeoutRunnable)
+                }
+            }
+
+    fun init() {
+        source.addCallback(sourceListener)
+    }
+
+    /**
+     * Starts the animation preemptively.
+     *
+     * - If the source provider is already running, this method won't change any behavior
+     * - If the source provider has not started running yet, it will call onTransitionStarted
+     *   for all listeners and optionally onTransitionProgress(initialProgress) if supplied.
+     *   When the source provider starts the animation it will switch to send progress and finished
+     *   events from it.
+     *   If the source provider won't start the animation within a timeout, the animation will be
+     *   cancelled and onTransitionFinished will be delivered to the current listeners.
+     */
+    @JvmOverloads
+    fun preemptivelyStartTransition(initialProgress: Float? = null) {
+        if (!isRunning) {
+            Trace.beginAsyncSection("$TAG#startedPreemptively", 0)
+
+            listeners.forEach { it.onTransitionStarted() }
+            initialProgress?.let { progress ->
+                listeners.forEach { it.onTransitionProgress(progress) }
+            }
+
+            handler.removeCallbacks(timeoutRunnable)
+            handler.postDelayed(timeoutRunnable, PREEMPTIVE_UNFOLD_TIMEOUT_MS)
+        }
+
+        isPreemptivelyRunning = true
+    }
+
+    fun cancelPreemptiveStart() {
+        handler.removeCallbacks(timeoutRunnable)
+        if (isRunning) {
+            listeners.forEach { it.onTransitionFinished() }
+        }
+        onPreemptiveStartFinished()
+    }
+
+    private fun onPreemptiveStartFinished() {
+        if (isPreemptivelyRunning) {
+            Trace.endAsyncSection("$TAG#startedPreemptively", 0)
+            isPreemptivelyRunning = false
+        }
+    }
+
+    override fun destroy() {
+        handler.removeCallbacks(timeoutRunnable)
+        source.removeCallback(sourceListener)
+        source.destroy()
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners += listener
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners -= listener
+    }
+}
+
+const val TAG = "PreemptiveUnfoldTransitionProgressProvider"
+const val PREEMPTIVE_UNFOLD_TIMEOUT_MS = 1700L
diff --git a/quickstep/tests/src/com/android/quickstep/util/unfold/PreemptiveUnfoldTransitionProgressProviderTest.kt b/quickstep/tests/src/com/android/quickstep/util/unfold/PreemptiveUnfoldTransitionProgressProviderTest.kt
new file mode 100644
index 0000000..f73be72
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/unfold/PreemptiveUnfoldTransitionProgressProviderTest.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util.unfold
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.util.Log
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.any
+import com.android.launcher3.util.mock
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class PreemptiveUnfoldTransitionProgressProviderTest {
+
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var source: TransitionProgressListener
+    private lateinit var handler: Handler
+    private lateinit var oldWtfHandler: Log.TerribleFailureHandler
+    private val listener: TransitionProgressListener = mock()
+    private val testWtfHandler: Log.TerribleFailureHandler = mock()
+
+    private lateinit var provider: PreemptiveUnfoldTransitionProgressProvider
+
+    @Before
+    fun before() {
+        testableLooper = TestableLooper.get(this)
+        handler = Handler(testableLooper.looper)
+
+        val testSource = createSource()
+        source = testSource as TransitionProgressListener
+
+        oldWtfHandler = Log.setWtfHandler(testWtfHandler)
+
+        provider = PreemptiveUnfoldTransitionProgressProvider(testSource, handler)
+        provider.init()
+        provider.addCallback(listener)
+    }
+
+    @After
+    fun after() {
+        Log.setWtfHandler(oldWtfHandler)
+    }
+
+    @Test
+    fun preemptiveStartInitialProgressNull_transitionStarts() {
+        provider.preemptivelyStartTransition(initialProgress = null)
+
+        verify(listener).onTransitionStarted()
+        verify(listener, never()).onTransitionProgress(anyFloat())
+    }
+
+    @Test
+    fun preemptiveStartWithInitialProgress_startsAnimationAndSendsProgress() {
+        provider.preemptivelyStartTransition(initialProgress = 0.5f)
+
+        verify(listener).onTransitionStarted()
+        verify(listener).onTransitionProgress(0.5f)
+    }
+
+    @Test
+    fun preemptiveStartAndCancel_finishesAnimation() {
+        provider.preemptivelyStartTransition()
+        provider.cancelPreemptiveStart()
+
+        with(inOrder(listener)) {
+            verify(listener).onTransitionStarted()
+            verify(listener).onTransitionFinished()
+        }
+    }
+
+    @Test
+    fun preemptiveStartAndThenSourceStartsTransition_transitionStarts() {
+        provider.preemptivelyStartTransition()
+        source.onTransitionStarted()
+
+        verify(listener).onTransitionStarted()
+    }
+
+    @Test
+    fun preemptiveStartAndThenSourceStartsAndFinishesTransition_transitionFinishes() {
+        provider.preemptivelyStartTransition()
+
+        source.onTransitionStarted()
+        source.onTransitionFinished()
+
+        with(inOrder(listener)) {
+            verify(listener).onTransitionStarted()
+            verify(listener).onTransitionFinished()
+        }
+    }
+
+    @Test
+    fun preemptiveStartAndThenSourceStartsAnimationAndSendsProgress_sendsProgress() {
+        provider.preemptivelyStartTransition()
+
+        source.onTransitionStarted()
+        source.onTransitionProgress(0.4f)
+
+        verify(listener).onTransitionProgress(0.4f)
+    }
+
+    @Test
+    fun preemptiveStartAndThenSourceSendsProgress_sendsProgress() {
+        provider.preemptivelyStartTransition()
+
+        source.onTransitionProgress(0.4f)
+
+        verify(listener).onTransitionProgress(0.4f)
+    }
+
+    @Test
+    fun preemptiveStartAfterTransitionRunning_transitionStarted() {
+        source.onTransitionStarted()
+
+        provider.preemptivelyStartTransition()
+
+        verify(listener).onTransitionStarted()
+    }
+
+    @Test
+    fun preemptiveStartAfterTransitionRunningAndThenFinished_transitionFinishes() {
+        source.onTransitionStarted()
+
+        provider.preemptivelyStartTransition()
+        source.onTransitionFinished()
+
+        with(inOrder(listener)) {
+            verify(listener).onTransitionStarted()
+            verify(listener).onTransitionFinished()
+        }
+    }
+
+    @Test
+    fun preemptiveStart_transitionDoesNotFinishAfterTimeout_finishesTransition() {
+        provider.preemptivelyStartTransition()
+
+        testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS + 1)
+        testableLooper.processAllMessages()
+
+        with(inOrder(listener)) {
+            verify(listener).onTransitionStarted()
+            verify(listener).onTransitionFinished()
+        }
+    }
+
+    @Test
+    fun preemptiveStart_transitionFinishAfterTimeout_logsWtf() {
+        provider.preemptivelyStartTransition()
+
+        testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS + 1)
+        testableLooper.processAllMessages()
+
+        verify(testWtfHandler).onTerribleFailure(any(), any(), anyBoolean())
+    }
+
+    @Test
+    fun preemptiveStart_transitionDoesNotFinishBeforeTimeout_doesNotFinishTransition() {
+        provider.preemptivelyStartTransition()
+
+        testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS - 1)
+        testableLooper.processAllMessages()
+
+        verify(listener).onTransitionStarted()
+    }
+
+    @Test
+    fun preemptiveStart_transitionStarted_timeoutHappened_doesNotFinishTransition() {
+        provider.preemptivelyStartTransition()
+
+        source.onTransitionStarted()
+        testableLooper.moveTimeForward(PREEMPTIVE_UNFOLD_TIMEOUT_MS + 1)
+        testableLooper.processAllMessages()
+
+        verify(listener).onTransitionStarted()
+    }
+
+    @Test
+    fun noPreemptiveStart_transitionStarted_startsTransition() {
+        source.onTransitionStarted()
+
+        verify(listener).onTransitionStarted()
+    }
+
+    @Test
+    fun noPreemptiveStart_transitionProgress_sendsProgress() {
+        source.onTransitionStarted()
+
+        source.onTransitionProgress(0.5f)
+
+        verify(listener).onTransitionProgress(0.5f)
+    }
+
+    @Test
+    fun noPreemptiveStart_transitionFinishes_finishesTransition() {
+        source.onTransitionStarted()
+        source.onTransitionProgress(0.5f)
+
+        source.onTransitionFinished()
+
+        with(inOrder(listener)) {
+            verify(listener).onTransitionStarted()
+            verify(listener).onTransitionFinished()
+        }
+    }
+
+    private fun createSource(): UnfoldTransitionProgressProvider =
+        object : TransitionProgressListener, UnfoldTransitionProgressProvider {
+
+            private val listeners = arrayListOf<TransitionProgressListener>()
+
+            override fun addCallback(listener: TransitionProgressListener) {
+                listeners += listener
+            }
+
+            override fun removeCallback(listener: TransitionProgressListener) {
+                listeners -= listener
+            }
+
+            override fun destroy() {}
+
+            override fun onTransitionStarted() =
+                listeners.forEach(TransitionProgressListener::onTransitionStarted)
+
+            override fun onTransitionFinishing() =
+                listeners.forEach(TransitionProgressListener::onTransitionFinishing)
+
+            override fun onTransitionFinished() =
+                listeners.forEach(TransitionProgressListener::onTransitionFinished)
+
+            override fun onTransitionProgress(progress: Float) =
+                listeners.forEach { it.onTransitionProgress(progress) }
+        }
+}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 331ae5d..fef6639 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -314,6 +314,12 @@
             "Enables receiving unfold animation events from sysui instead of calculating "
                     + "them in launcher process using hinge sensor values.");
 
+    public static final BooleanFlag PREEMPTIVE_UNFOLD_ANIMATION_START = getDebugFlag(270397209,
+            "PREEMPTIVE_UNFOLD_ANIMATION_START", ENABLED,
+            "Enables starting the unfold animation preemptively when unfolding, without"
+                    + "waiting for SystemUI and then merging the SystemUI progress whenever we "
+                    + "start receiving the events");
+
     // TODO(Block 23): Clean up flags
     public static final BooleanFlag ENABLE_GRID_ONLY_OVERVIEW = getDebugFlag(270397206,
             "ENABLE_GRID_ONLY_OVERVIEW", DISABLED,