Monitor fold to AOD latency

This logs the latency from when we receive the device state change (device folded), to when the AOD animation starts.

Bug: 237270816
Test: atest FoldAodAnimationControllerTest
Change-Id: I67e0e04bbc24064640f46cdc8feb667814170529
Merged-In: I67e0e04bbc24064640f46cdc8feb667814170529
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index ca40a40..14a6d5e 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -142,6 +142,11 @@
      */
     public static final int ACTION_LOAD_SHARE_SHEET = 16;
 
+    /**
+     * Time it takes to show AOD display after folding the device.
+     */
+    public static final int ACTION_FOLD_TO_AOD = 17;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -160,6 +165,7 @@
         ACTION_UDFPS_ILLUMINATE,
         ACTION_SHOW_BACK_ARROW,
         ACTION_LOAD_SHARE_SHEET,
+        ACTION_FOLD_TO_AOD,
     };
 
     /** @hide */
@@ -181,6 +187,7 @@
         ACTION_UDFPS_ILLUMINATE,
         ACTION_SHOW_BACK_ARROW,
         ACTION_LOAD_SHARE_SHEET,
+        ACTION_FOLD_TO_AOD
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -204,6 +211,7 @@
             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_UDFPS_ILLUMINATE,
             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW,
             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET,
+            FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD
     };
 
     private static LatencyTracker sLatencyTracker;
@@ -297,6 +305,8 @@
                 return "ACTION_SHOW_BACK_ARROW";
             case 17:
                 return "ACTION_LOAD_SHARE_SHEET";
+            case 19:
+                return "ACTION_FOLD_TO_AOD";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 4e74540f..58e9bb8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3881,29 +3881,37 @@
     }
 
     /**
-     * Starts fold to AOD animation
+     * Starts fold to AOD animation.
+     *
+     * @param startAction invoked when the animation starts.
+     * @param endAction invoked when the animation finishes, also if it was cancelled.
+     * @param cancelAction invoked when the animation is cancelled, before endAction.
      */
-    public void startFoldToAodAnimation(Runnable endAction) {
+    public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
+            Runnable cancelAction) {
         mView.animate()
-                .translationX(0)
-                .alpha(1f)
-                .setDuration(ANIMATION_DURATION_FOLD_TO_AOD)
-                .setInterpolator(EMPHASIZED_DECELERATE)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationCancel(Animator animation) {
-                        endAction.run();
-                    }
+            .translationX(0)
+            .alpha(1f)
+            .setDuration(ANIMATION_DURATION_FOLD_TO_AOD)
+            .setInterpolator(EMPHASIZED_DECELERATE)
+            .setListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    startAction.run();
+                }
 
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        endAction.run();
-                    }
-                })
-                .setUpdateListener(anim -> {
-                    mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction());
-                })
-                .start();
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    cancelAction.run();
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    endAction.run();
+                }
+            }).setUpdateListener(anim -> {
+                mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction());
+            }).start();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index d6dfcea..8f127fd 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -22,11 +22,12 @@
 import android.os.PowerManager
 import android.provider.Settings
 import androidx.core.view.OneShotPreDrawListener
+import com.android.internal.util.LatencyTracker
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.statusbar.LightRevealScrim
-import com.android.systemui.statusbar.phone.ScreenOffAnimation
 import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.android.systemui.statusbar.phone.ScreenOffAnimation
 import com.android.systemui.statusbar.policy.CallbackController
 import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationStatus
 import com.android.systemui.util.settings.GlobalSettings
@@ -47,7 +48,8 @@
     private val context: Context,
     private val deviceStateManager: DeviceStateManager,
     private val wakefulnessLifecycle: WakefulnessLifecycle,
-    private val globalSettings: GlobalSettings
+    private val globalSettings: GlobalSettings,
+    private val latencyTracker: LatencyTracker,
 ) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer {
 
     private lateinit var mCentralSurfaces: CentralSurfaces
@@ -64,12 +66,14 @@
     private var isAnimationPlaying = false
 
     private val statusListeners = arrayListOf<FoldAodAnimationStatus>()
+    private val foldToAodLatencyTracker = FoldToAodLatencyTracker()
 
     private val startAnimationRunnable = Runnable {
-        mCentralSurfaces.notificationPanelViewController.startFoldToAodAnimation {
-            // End action
-            setAnimationState(playing = false)
-        }
+        mCentralSurfaces.notificationPanelViewController.startFoldToAodAnimation(
+            /* startAction= */ { foldToAodLatencyTracker.onAnimationStarted() },
+            /* endAction= */ { setAnimationState(playing = false) },
+            /* cancelAction= */ { setAnimationState(playing = false) },
+        )
     }
 
     override fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
@@ -82,11 +86,13 @@
     /** Returns true if we should run fold to AOD animation */
     override fun shouldPlayAnimation(): Boolean = shouldPlayAnimation
 
-    override fun startAnimation(): Boolean =
-        if (alwaysOnEnabled &&
+    private fun shouldStartAnimation(): Boolean =
+        alwaysOnEnabled &&
             wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD &&
             globalSettings.getString(Settings.Global.ANIMATOR_DURATION_SCALE) != "0"
-        ) {
+
+    override fun startAnimation(): Boolean =
+        if (shouldStartAnimation()) {
             setAnimationState(playing = true)
             mCentralSurfaces.notificationPanelViewController.prepareFoldToAodAnimation()
             true
@@ -97,6 +103,7 @@
 
     override fun onStartedWakingUp() {
         if (isAnimationPlaying) {
+            foldToAodLatencyTracker.cancel()
             handler.removeCallbacks(startAnimationRunnable)
             mCentralSurfaces.notificationPanelViewController.cancelFoldToAodAnimation()
         }
@@ -137,7 +144,8 @@
             // but we should wait for the initial animation preparations to be drawn
             // (setting initial alpha/translation)
             OneShotPreDrawListener.add(
-                mCentralSurfaces.notificationPanelViewController.view, onReady
+                mCentralSurfaces.notificationPanelViewController.view,
+                onReady
             )
         } else {
             // No animation, call ready callback immediately
@@ -209,5 +217,41 @@
                     isFoldHandled = false
                 }
                 this.isFolded = isFolded
-            })
+                if (isFolded) {
+                    foldToAodLatencyTracker.onFolded()
+                }
+            }
+        )
+
+    /**
+     * Tracks the latency of fold to AOD using [LatencyTracker].
+     *
+     * Events that trigger start and end are:
+     *
+     * - Start: Once [DeviceStateManager] sends the folded signal [FoldToAodLatencyTracker.onFolded]
+     * is called and latency tracking starts.
+     * - End: Once the fold -> AOD animation starts, [FoldToAodLatencyTracker.onAnimationStarted] is
+     * called, and latency tracking stops.
+     */
+    private inner class FoldToAodLatencyTracker {
+
+        /** Triggers the latency logging, if needed. */
+        fun onFolded() {
+            if (shouldStartAnimation()) {
+                latencyTracker.onActionStart(LatencyTracker.ACTION_FOLD_TO_AOD)
+            }
+        }
+        /**
+         * Called once the Fold -> AOD animation is started.
+         *
+         * For latency tracking, this determines the end of the fold to aod action.
+         */
+        fun onAnimationStarted() {
+            latencyTracker.onActionEnd(LatencyTracker.ACTION_FOLD_TO_AOD)
+        }
+
+        fun cancel() {
+            latencyTracker.onActionCancel(LatencyTracker.ACTION_FOLD_TO_AOD)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
new file mode 100644
index 0000000..f51f783
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2022 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.hardware.devicestate.DeviceStateManager
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener
+import android.os.Handler
+import android.os.PowerManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import com.android.systemui.unfold.util.FoldableDeviceStates
+import com.android.systemui.unfold.util.FoldableTestUtils
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.settings.GlobalSettings
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class FoldAodAnimationControllerTest : SysuiTestCase() {
+
+    @Mock lateinit var deviceStateManager: DeviceStateManager
+
+    @Mock lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+
+    @Mock lateinit var globalSettings: GlobalSettings
+
+    @Mock lateinit var latencyTracker: LatencyTracker
+
+    @Mock lateinit var centralSurfaces: CentralSurfaces
+
+    @Mock lateinit var lightRevealScrim: LightRevealScrim
+
+    @Mock lateinit var notificationPanelViewController: NotificationPanelViewController
+
+    @Mock lateinit var viewGroup: ViewGroup
+
+    @Mock lateinit var viewTreeObserver: ViewTreeObserver
+
+    @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
+
+    private lateinit var deviceStates: FoldableDeviceStates
+
+    private lateinit var testableLooper: TestableLooper
+
+    lateinit var foldAodAnimationController: FoldAodAnimationController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        testableLooper = TestableLooper.get(this)
+
+        foldAodAnimationController =
+            FoldAodAnimationController(
+                    Handler(testableLooper.looper),
+                    context.mainExecutor,
+                    context,
+                    deviceStateManager,
+                    wakefulnessLifecycle,
+                    globalSettings,
+                    latencyTracker,
+                )
+                .apply { initialize(centralSurfaces, lightRevealScrim) }
+        deviceStates = FoldableTestUtils.findDeviceStates(context)
+
+        whenever(notificationPanelViewController.view).thenReturn(viewGroup)
+        whenever(viewGroup.viewTreeObserver).thenReturn(viewTreeObserver)
+        whenever(wakefulnessLifecycle.lastSleepReason)
+            .thenReturn(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD)
+        whenever(centralSurfaces.notificationPanelViewController)
+            .thenReturn(notificationPanelViewController)
+        whenever(notificationPanelViewController.startFoldToAodAnimation(any(), any(), any()))
+            .then {
+                val onActionStarted = it.arguments[0] as Runnable
+                onActionStarted.run()
+            }
+        verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
+
+        foldAodAnimationController.setIsDozing(dozing = true)
+        setAodEnabled(enabled = true)
+        sendFoldEvent(folded = false)
+    }
+
+    @Test
+    fun onFolded_aodDisabled_doesNotLogLatency() {
+        setAodEnabled(enabled = false)
+
+        fold()
+        simulateScreenTurningOn()
+
+        verifyNoMoreInteractions(latencyTracker)
+    }
+
+    @Test
+    fun onFolded_aodEnabled_logsLatency() {
+        setAodEnabled(enabled = true)
+
+        fold()
+        simulateScreenTurningOn()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker).onActionEnd(any())
+    }
+
+    @Test
+    fun onFolded_animationCancelled_doesNotLogLatency() {
+        setAodEnabled(enabled = true)
+
+        fold()
+        foldAodAnimationController.onScreenTurningOn({})
+        foldAodAnimationController.onStartedWakingUp()
+        testableLooper.processAllMessages()
+
+        verify(latencyTracker).onActionStart(any())
+        verify(latencyTracker).onActionCancel(any())
+    }
+
+    private fun simulateScreenTurningOn() {
+        foldAodAnimationController.onScreenTurningOn({})
+        foldAodAnimationController.onScreenTurnedOn()
+        testableLooper.processAllMessages()
+    }
+
+    private fun fold() = sendFoldEvent(folded = true)
+
+    private fun setAodEnabled(enabled: Boolean) =
+        foldAodAnimationController.onAlwaysOnChanged(alwaysOn = enabled)
+
+    private fun sendFoldEvent(folded: Boolean) {
+        val state = if (folded) deviceStates.folded else deviceStates.unfolded
+        foldStateListenerCaptor.value.onStateChanged(state)
+    }
+}