Merge "Fix Notification clipping flicker during AOD=>LS" into udc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ae7c216..b0f3f59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -93,6 +93,12 @@
     private boolean mAppearing;
     private float mPulseHeight = MAX_PULSE_HEIGHT;
 
+    /**
+     * The ExpandableNotificationRow that is pulsing, or the one that was pulsing
+     * when the device started to transition from AOD to LockScreen.
+     */
+    private ExpandableNotificationRow mPulsingRow;
+
     /** Fraction of lockscreen to shade animation (on lockscreen swipe down). */
     private float mFractionToShade;
 
@@ -564,6 +570,19 @@
         return mPulsing && entry.isAlerting();
     }
 
+    public void setPulsingRow(ExpandableNotificationRow row) {
+        mPulsingRow = row;
+    }
+
+    /**
+     * @param row The row to check
+     * @return true if row is the pulsing row when the device started to transition from AOD to lock
+     * screen
+     */
+    public boolean isPulsingRow(ExpandableView row) {
+        return mPulsingRow == row;
+    }
+
     public boolean isPanelTracking() {
         return mPanelTracking;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 92d767a..6f1c378 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -548,7 +548,7 @@
         ExpandableViewState viewState = view.getViewState();
         viewState.location = ExpandableViewState.LOCATION_UNKNOWN;
 
-        final float expansionFraction = getExpansionFractionWithoutShelf(
+        float expansionFraction = getExpansionFractionWithoutShelf(
                 algorithmState, ambientState);
 
         // Add gap between sections.
@@ -619,6 +619,11 @@
                     updateViewWithShelf(view, viewState, shelfStart);
                 }
             }
+            // Avoid pulsing notification flicker during AOD to LS
+            // A pulsing notification is already expanded, no need to expand it again with animation
+            if (ambientState.isPulsingRow(view)) {
+                expansionFraction = 1.0f;
+            }
             // Clip height of view right before shelf.
             viewState.height = (int) (getMaxAllowedChildHeight(view) * expansionFraction);
         }
@@ -700,9 +705,11 @@
                 && !(child instanceof FooterView);
     }
 
-    private void updatePulsingStates(StackScrollAlgorithmState algorithmState,
+    @VisibleForTesting
+    void updatePulsingStates(StackScrollAlgorithmState algorithmState,
                                      AmbientState ambientState) {
         int childCount = algorithmState.visibleChildren.size();
+        ExpandableNotificationRow pulsingRow = null;
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
             if (!(child instanceof ExpandableNotificationRow)) {
@@ -714,6 +721,19 @@
             }
             ExpandableViewState viewState = row.getViewState();
             viewState.hidden = false;
+            pulsingRow = row;
+        }
+
+        // Set AmbientState#pulsingRow to the current pulsing row when on AOD.
+        // Set AmbientState#pulsingRow=null when on lockscreen, since AmbientState#pulsingRow
+        // is only used for skipping the unfurl animation for (the notification that was already
+        // showing at full height on AOD) during the AOD=>lockscreen transition, where
+        // dozeAmount=[1f, 0f). We also need to reset the pulsingRow once it is no longer used
+        // because it will interfere with future unfurling animations - for example, during the
+        // LS=>AOD animation, the pulsingRow may stay at full height when it should squish with the
+        // rest of the stack.
+        if (ambientState.getDozeAmount() == 0.0f || ambientState.getDozeAmount() == 1.0f) {
+            ambientState.setPulsingRow(pulsingRow);
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 7f20f1e..e12d179 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -716,6 +716,94 @@
                 .isLessThan(px(R.dimen.heads_up_pinned_elevation))
     }
 
+    @Test
+    fun aodToLockScreen_hasPulsingNotification_pulsingNotificationRowDoesNotChange() {
+        // Given: Before AOD to LockScreen, there was a pulsing notification
+        val pulsingNotificationView = createPulsingViewMock()
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(pulsingNotificationView)
+        ambientState.setPulsingRow(pulsingNotificationView)
+
+        // When: during AOD to LockScreen, any dozeAmount between (0, 1.0) is equivalent as a middle
+        // stage; here we use 0.5 for testing.
+        // stackScrollAlgorithm.updatePulsingStates is called
+        ambientState.dozeAmount = 0.5f
+        stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState)
+
+        // Then: ambientState.pulsingRow should still be pulsingNotificationView
+        assertTrue(ambientState.isPulsingRow(pulsingNotificationView))
+    }
+
+    @Test
+    fun deviceOnAod_hasPulsingNotification_recordPulsingNotificationRow() {
+        // Given: Device is on AOD, there is a pulsing notification
+        // ambientState.pulsingRow is null before stackScrollAlgorithm.updatePulsingStates
+        ambientState.dozeAmount = 1.0f
+        val pulsingNotificationView = createPulsingViewMock()
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(pulsingNotificationView)
+        ambientState.setPulsingRow(null)
+
+        // When: stackScrollAlgorithm.updatePulsingStates is called
+        stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState)
+
+        // Then: ambientState.pulsingRow should record the pulsingNotificationView
+        assertTrue(ambientState.isPulsingRow(pulsingNotificationView))
+    }
+
+    @Test
+    fun deviceOnLockScreen_hasPulsingNotificationBefore_clearPulsingNotificationRowRecord() {
+        // Given: Device finished AOD to LockScreen, there was a pulsing notification, and
+        // ambientState.pulsingRow was not null before AOD to LockScreen
+        // pulsingNotificationView.showingPulsing() returns false since the device is on LockScreen
+        ambientState.dozeAmount = 0.0f
+        val pulsingNotificationView = createPulsingViewMock()
+        whenever(pulsingNotificationView.showingPulsing()).thenReturn(false)
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(pulsingNotificationView)
+        ambientState.setPulsingRow(pulsingNotificationView)
+
+        // When: stackScrollAlgorithm.updatePulsingStates is called
+        stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState)
+
+        // Then: ambientState.pulsingRow should be null
+        assertTrue(ambientState.isPulsingRow(null))
+    }
+
+    @Test
+    fun aodToLockScreen_hasPulsingNotification_pulsingNotificationRowShowAtFullHeight() {
+        // Given: Before AOD to LockScreen, there was a pulsing notification
+        val pulsingNotificationView = createPulsingViewMock()
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(pulsingNotificationView)
+        ambientState.setPulsingRow(pulsingNotificationView)
+
+        // When: during AOD to LockScreen, any dozeAmount between (0, 1.0) is equivalent as a middle
+        // stage; here we use 0.5 for testing. The expansionFraction is also 0.5.
+        // stackScrollAlgorithm.resetViewStates is called.
+        ambientState.dozeAmount = 0.5f
+        setExpansionFractionWithoutShelfDuringAodToLockScreen(
+                ambientState,
+                algorithmState,
+                fraction = 0.5f
+        )
+        stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+        // Then: pulsingNotificationView should show at full height
+        assertEquals(
+                stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView),
+                pulsingNotificationView.viewState.height
+        )
+
+        // After: reset dozeAmount and expansionFraction
+        ambientState.dozeAmount = 0f
+        setExpansionFractionWithoutShelfDuringAodToLockScreen(
+                ambientState,
+                algorithmState,
+                fraction = 1f
+        )
+    }
+
     private fun createHunViewMock(
             isShadeOpen: Boolean,
             fullyVisible: Boolean,
@@ -744,6 +832,29 @@
                 headsUpIsVisible = fullyVisible
             }
 
+    private fun createPulsingViewMock(
+    ) =
+            mock<ExpandableNotificationRow>().apply {
+                whenever(this.viewState).thenReturn(ExpandableViewState())
+                whenever(this.showingPulsing()).thenReturn(true)
+            }
+
+    private fun setExpansionFractionWithoutShelfDuringAodToLockScreen(
+            ambientState: AmbientState,
+            algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState,
+            fraction: Float
+    ) {
+        // showingShelf: false
+        algorithmState.firstViewInShelf = null
+        // scrimPadding: 0, because device is on lock screen
+        ambientState.setStatusBarState(StatusBarState.KEYGUARD)
+        ambientState.dozeAmount = 0.0f
+        // set stackEndHeight and stackHeight
+        // ExpansionFractionWithoutShelf == stackHeight / stackEndHeight
+        ambientState.stackEndHeight = 100f
+        ambientState.stackHeight = ambientState.stackEndHeight * fraction
+    }
+
     private fun resetViewStates_expansionChanging_notificationAlphaUpdated(
             expansionFraction: Float,
             expectedAlpha: Float,