Fix missing Notification collapse animation on LS

When we start to expand a Notification on the LS but we abort it
halfway, the Notification should animate back to the collapsed
state. This animation was missing, because we tried to access the
"ExpandableView#actualHeight" property via reflection.

Test: create a makepush build and observe the animation on the LS
Test: atest DragDownHelperTest
Test: atest PulseExpansionHandlerTest

Fixes: 237084760
Change-Id: I3798dec1ab016d89b61e87a3192199ea16181472
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 74d8f1b..d3343df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -2,7 +2,6 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
-import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
 import android.content.Context
 import android.content.res.Configuration
@@ -880,15 +879,22 @@
         child.actualHeight = (child.collapsedHeight + rubberband).toInt()
     }
 
-    private fun cancelChildExpansion(child: ExpandableView) {
+    @VisibleForTesting
+    fun cancelChildExpansion(
+            child: ExpandableView,
+            animationDuration: Long = SPRING_BACK_ANIMATION_LENGTH_MS
+    ) {
         if (child.actualHeight == child.collapsedHeight) {
             expandCallback.setUserLockedChild(child, false)
             return
         }
-        val anim = ObjectAnimator.ofInt(child, "actualHeight",
-                child.actualHeight, child.collapsedHeight)
+        val anim = ValueAnimator.ofInt(child.actualHeight, child.collapsedHeight)
         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
-        anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS
+        anim.duration = animationDuration
+        anim.addUpdateListener { animation: ValueAnimator ->
+            // don't use reflection, because the `actualHeight` field may be obfuscated
+            child.actualHeight = animation.animatedValue as Int
+        }
         anim.addListener(object : AnimatorListenerAdapter() {
             override fun onAnimationEnd(animation: Animator) {
                 expandCallback.setUserLockedChild(child, false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index 6302ef1..bbff046 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -18,7 +18,7 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
-import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
 import android.content.Context
 import android.content.res.Configuration
 import android.os.PowerManager
@@ -28,6 +28,7 @@
 import android.view.MotionEvent
 import android.view.VelocityTracker
 import android.view.ViewConfiguration
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
@@ -276,15 +277,22 @@
         }
     }
 
-    private fun reset(child: ExpandableView) {
+    @VisibleForTesting
+    fun reset(
+            child: ExpandableView,
+            animationDuration: Long = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
+    ) {
         if (child.actualHeight == child.collapsedHeight) {
             setUserLocked(child, false)
             return
         }
-        val anim = ObjectAnimator.ofInt(child, "actualHeight",
-                child.actualHeight, child.collapsedHeight)
+        val anim = ValueAnimator.ofInt(child.actualHeight, child.collapsedHeight)
         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
-        anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
+        anim.duration = animationDuration
+        anim.addUpdateListener { animation: ValueAnimator ->
+            // don't use reflection, because the `actualHeight` field may be obfuscated
+            child.actualHeight = animation.animatedValue as Int
+        }
         anim.addListener(object : AnimatorListenerAdapter() {
             override fun onAnimationEnd(animation: Animator) {
                 setUserLocked(child, false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt
new file mode 100644
index 0000000..d925d0a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.statusbar
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.ExpandHelper
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.atLeast
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DragDownHelperTest : SysuiTestCase() {
+
+    private lateinit var dragDownHelper: DragDownHelper
+
+    private val collapsedHeight = 300
+    private val falsingManager: FalsingManager = mock()
+    private val falsingCollector: FalsingCollector = mock()
+    private val dragDownloadCallback: LockscreenShadeTransitionController = mock()
+    private val expandableView: ExpandableView = mock()
+    private val expandCallback: ExpandHelper.Callback = mock()
+
+    @Before
+    fun setUp() {
+        whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
+
+        dragDownHelper = DragDownHelper(
+                falsingManager,
+                falsingCollector,
+                dragDownloadCallback,
+                mContext
+        ).also {
+            it.expandCallback = expandCallback
+        }
+    }
+
+    @Test
+    fun cancelChildExpansion_updateHeight() {
+        whenever(expandableView.actualHeight).thenReturn(500)
+
+        dragDownHelper.cancelChildExpansion(expandableView, animationDuration = 0)
+
+        verify(expandableView, atLeast(1)).actualHeight = collapsedHeight
+    }
+
+    @Test
+    fun cancelChildExpansion_dontUpdateHeight() {
+        whenever(expandableView.actualHeight).thenReturn(collapsedHeight)
+
+        dragDownHelper.cancelChildExpansion(expandableView, animationDuration = 0)
+
+        verify(expandableView, never()).actualHeight = anyInt()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
new file mode 100644
index 0000000..44cbe51
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.statusbar
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.atLeast
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class PulseExpansionHandlerTest : SysuiTestCase() {
+
+    private lateinit var pulseExpansionHandler: PulseExpansionHandler
+
+    private val collapsedHeight = 300
+    private val wakeUpCoordinator: NotificationWakeUpCoordinator = mock()
+    private val bypassController: KeyguardBypassController = mock()
+    private val headsUpManager: HeadsUpManagerPhone = mock()
+    private val roundnessManager: NotificationRoundnessManager = mock()
+    private val configurationController: ConfigurationController = mock()
+    private val statusBarStateController: StatusBarStateController = mock()
+    private val falsingManager: FalsingManager = mock()
+    private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock()
+    private val falsingCollector: FalsingCollector = mock()
+    private val dumpManager: DumpManager = mock()
+    private val expandableView: ExpandableView = mock()
+
+    @Before
+    fun setUp() {
+        whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
+
+        pulseExpansionHandler = PulseExpansionHandler(
+                mContext,
+                wakeUpCoordinator,
+                bypassController,
+                headsUpManager,
+                roundnessManager,
+                configurationController,
+                statusBarStateController,
+                falsingManager,
+                lockscreenShadeTransitionController,
+                falsingCollector,
+                dumpManager
+        )
+    }
+
+    @Test
+    fun resetChild_updateHeight() {
+        whenever(expandableView.actualHeight).thenReturn(500)
+
+        pulseExpansionHandler.reset(expandableView, animationDuration = 0)
+
+        verify(expandableView, atLeast(1)).actualHeight = collapsedHeight
+    }
+
+    @Test
+    fun resetChild_dontUpdateHeight() {
+        whenever(expandableView.actualHeight).thenReturn(collapsedHeight)
+
+        pulseExpansionHandler.reset(expandableView, animationDuration = 0)
+
+        verify(expandableView, never()).actualHeight = anyInt()
+    }
+}