[Unfold transition] Update latency tracker to listen for animation start

We are going to introduce potential delay before
starting the animation, so updating the latency
tracker to track the end of the unfold display switch
only when the animation is started.

Bug: 258214245
Test: atest com.android.systemui.unfold.UnfoldLatencyTrackerTest
Test: manual unfold with/without animations => check that metric is reported
Change-Id: If3017719631f115579f16493136cce0d0e5e2f80
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
index 0b04fb4..6063ce2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
@@ -17,6 +17,7 @@
 
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
+import com.android.systemui.util.traceSection
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -39,14 +40,22 @@
     }
 
     override fun onScreenTurnedOn() {
-        listeners.forEach(ScreenListener::onScreenTurnedOn)
+        traceSection("$TRACE_TAG#onScreenTurnedOn") {
+            listeners.forEach(ScreenListener::onScreenTurnedOn)
+        }
     }
 
     override fun onScreenTurningOff() {
-        listeners.forEach(ScreenListener::onScreenTurningOff)
+        traceSection("$TRACE_TAG#onScreenTurningOff") {
+            listeners.forEach(ScreenListener::onScreenTurningOff)
+        }
     }
 
     override fun onScreenTurningOn() {
-        listeners.forEach(ScreenListener::onScreenTurningOn)
+        traceSection("$TRACE_TAG#onScreenTurningOn") {
+            listeners.forEach(ScreenListener::onScreenTurningOn)
+        }
     }
 }
+
+private const val TRACE_TAG = "LifecycleScreenStatusProvider"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
index 79b42b8..9269df3 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
@@ -16,12 +16,18 @@
 
 package com.android.systemui.unfold
 
+import android.content.ContentResolver
 import android.content.Context
 import android.hardware.devicestate.DeviceStateManager
+import android.util.Log
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.keyguard.ScreenLifecycle
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
+import com.android.systemui.util.Compile
+import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -41,17 +47,19 @@
 constructor(
     private val latencyTracker: LatencyTracker,
     private val deviceStateManager: DeviceStateManager,
+    private val transitionProgressProvider: Optional<UnfoldTransitionProgressProvider>,
     @UiBackground private val uiBgExecutor: Executor,
     private val context: Context,
+    private val contentResolver: ContentResolver,
     private val screenLifecycle: ScreenLifecycle
-) : ScreenLifecycle.Observer {
+) : ScreenLifecycle.Observer, TransitionProgressListener {
 
     private var folded: Boolean? = null
+    private var isTransitionEnabled: Boolean? = null
     private val foldStateListener = FoldStateListener(context)
     private val isFoldable: Boolean
         get() =
-            context
-                .resources
+            context.resources
                 .getIntArray(com.android.internal.R.array.config_foldedDeviceStates)
                 .isNotEmpty()
 
@@ -62,6 +70,11 @@
         }
         deviceStateManager.registerCallback(uiBgExecutor, foldStateListener)
         screenLifecycle.addObserver(this)
+        if (transitionProgressProvider.isPresent) {
+            // Might not be present if the device is not a foldable device or unfold transition
+            // is disabled in the device configuration
+            transitionProgressProvider.get().addCallback(this)
+        }
     }
 
     /**
@@ -71,16 +84,72 @@
      * end action event only if we previously received a fold state.
      */
     override fun onScreenTurnedOn() {
-        if (folded == false) {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+            )
+        }
+
+        // We use onScreenTurnedOn event to finish tracking only if we are not playing
+        // the unfold animation (e.g. it could be disabled because of battery saver).
+        // When animation is enabled finishing of the tracking will be done in onTransitionStarted.
+        if (folded == false && isTransitionEnabled == false) {
             latencyTracker.onActionEnd(LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD)
+
+            if (DEBUG) {
+                Log.d(TAG, "onScreenTurnedOn: ending ACTION_SWITCH_DISPLAY_UNFOLD")
+            }
+        }
+    }
+
+    /**
+     * This callback is used to end the metric when the unfold animation is enabled because it could
+     * add an additional delay to synchronize with launcher.
+     */
+    override fun onTransitionStarted() {
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+            )
+        }
+
+        if (folded == false && isTransitionEnabled == true) {
+            latencyTracker.onActionEnd(LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD)
+
+            if (DEBUG) {
+                Log.d(TAG, "onTransitionStarted: ending ACTION_SWITCH_DISPLAY_UNFOLD")
+            }
         }
     }
 
     private fun onFoldEvent(folded: Boolean) {
-        if (this.folded != folded) {
+        val oldFolded = this.folded
+
+        if (oldFolded != folded) {
             this.folded = folded
-            if (!folded) { // unfolding started
+
+            if (DEBUG) {
+                Log.d(TAG, "Received onFoldEvent = $folded")
+            }
+
+            // Do not start tracking when oldFolded is null, this means that this is the first
+            // onFoldEvent after booting the device or starting SystemUI and not actual folding or
+            // unfolding the device.
+            if (oldFolded != null && !folded) {
+                // Unfolding started
                 latencyTracker.onActionStart(LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD)
+                isTransitionEnabled =
+                    transitionProgressProvider.isPresent && contentResolver.areAnimationsEnabled()
+
+                if (DEBUG) {
+                    Log.d(
+                        TAG,
+                        "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " +
+                            "isTransitionEnabled = $isTransitionEnabled"
+                    )
+                }
             }
         }
     }
@@ -88,3 +157,6 @@
     private inner class FoldStateListener(context: Context) :
         DeviceStateManager.FoldStateListener(context, { onFoldEvent(it) })
 }
+
+private const val TAG = "UnfoldLatencyTracker"
+private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
index 4e2736c7..c7dc0ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.devicestate.DeviceStateManager
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
@@ -32,9 +33,11 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
+import java.util.Optional
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -59,14 +62,18 @@
 
     private lateinit var unfoldLatencyTracker: UnfoldLatencyTracker
 
+    private val transitionProgressProvider = TestUnfoldTransitionProvider()
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         unfoldLatencyTracker = UnfoldLatencyTracker(
             latencyTracker,
             deviceStateManager,
+            Optional.of(transitionProgressProvider),
             context.mainExecutor,
             context,
+            context.contentResolver,
             screenLifecycle
         ).apply { init() }
         deviceStates = FoldableTestUtils.findDeviceStates(context)
@@ -76,8 +83,11 @@
     }
 
     @Test
-    fun unfold_eventPropagated() {
+    fun unfold_startedFolded_animationsDisabled_eventPropagatedOnScreenTurnedOnEvent() {
+        setAnimationsEnabled(false)
+        sendFoldEvent(folded = true)
         sendFoldEvent(folded = false)
+
         sendScreenTurnedOnEvent()
 
         verify(latencyTracker).onActionStart(any())
@@ -85,14 +95,77 @@
     }
 
     @Test
-    fun fold_eventNotPropagated() {
+    fun unfold_startedFolded_animationsEnabledOnScreenTurnedOn_eventNotFinished() {
+        setAnimationsEnabled(true)
         sendFoldEvent(folded = true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker, never()).onActionEnd(any())
+    }
+
+    @Test
+    fun unfold_firstFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventNotPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+        transitionProgressProvider.onTransitionStarted()
+
+        verifyNoMoreInteractions(latencyTracker)
+    }
+
+    @Test
+    fun unfold_secondFoldEventAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+        transitionProgressProvider.onTransitionStarted()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker).onActionEnd(any())
+    }
+
+    @Test
+    fun unfold_unfoldFoldUnfoldAnimationsEnabledOnScreenTurnedOnAndTransitionStarted_eventPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = false)
+        sendFoldEvent(folded = true)
+        sendFoldEvent(folded = false)
+
+        sendScreenTurnedOnEvent()
+        transitionProgressProvider.onTransitionStarted()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker).onActionEnd(any())
+    }
+
+    @Test
+    fun fold_animationsDisabled_screenTurnedOn_eventNotPropagated() {
+        setAnimationsEnabled(false)
+        sendFoldEvent(folded = true)
+
         sendScreenTurnedOnEvent() // outer display on.
 
         verifyNoMoreInteractions(latencyTracker)
     }
 
     @Test
+    fun fold_animationsEnabled_screenTurnedOn_eventNotPropagated() {
+        setAnimationsEnabled(true)
+        sendFoldEvent(folded = true)
+
+        sendScreenTurnedOnEvent() // outer display on.
+        transitionProgressProvider.onTransitionStarted()
+
+        verifyNoMoreInteractions(latencyTracker)
+    }
+
+    @Test
     fun onScreenTurnedOn_stateNeverSet_eventNotPropagated() {
         sendScreenTurnedOnEvent()
 
@@ -107,4 +180,20 @@
     private fun sendScreenTurnedOnEvent() {
         screenLifecycleCaptor.value.onScreenTurnedOn()
     }
+
+    private fun setAnimationsEnabled(enabled: Boolean) {
+        val durationScale =
+            if (enabled) {
+                1f
+            } else {
+                0f
+            }
+
+        // It uses [TestableSettingsProvider] and it will be cleared after the test
+        Settings.Global.putString(
+            context.contentResolver,
+            Settings.Global.ANIMATOR_DURATION_SCALE,
+            durationScale.toString()
+        )
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index ecc029d..074b1e1 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.unfold.progress
 
 import android.os.Trace
+import android.os.Trace.TRACE_TAG_APP
 import android.util.Log
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -157,7 +158,10 @@
     }
 
     private fun onStartTransition() {
+        Trace.beginSection( "$TAG#onStartTransition")
         listeners.forEach { it.onTransitionStarted() }
+        Trace.endSection()
+
         isTransitionRunning = true
 
         if (DEBUG) {