Fix onStart/EndAction() for ViewPropertyAnimatorCompat

These methods are meant to be single-action only (removed when they
are activated). That's the way they work on the framework; it was an oversight
to have them be persistent in the ICS version of the support library.

Issue #25764942  ViewPropertyAnimatorCompatICS end action is not cleared

Change-Id: I0825330c76cecf63960d79f74116acd99b874830
(cherry picked from commit fdd92b54cc21d9927ee89cf675cf4c90c306f24a)
diff --git a/v4/java/android/support/v4/view/ViewPropertyAnimatorCompat.java b/v4/java/android/support/v4/view/ViewPropertyAnimatorCompat.java
index a28c101..3427e26 100644
--- a/v4/java/android/support/v4/view/ViewPropertyAnimatorCompat.java
+++ b/v4/java/android/support/v4/view/ViewPropertyAnimatorCompat.java
@@ -298,6 +298,8 @@
             }
             Runnable startAction = vpa.mStartAction;
             Runnable endAction = vpa.mEndAction;
+            vpa.mStartAction = null;
+            vpa.mEndAction = null;
             if (startAction != null) {
                 startAction.run();
             }
@@ -538,7 +540,9 @@
                     ViewCompat.setLayerType(view, ViewCompat.LAYER_TYPE_HARDWARE, null);
                 }
                 if (mVpa.mStartAction != null) {
-                    mVpa.mStartAction.run();
+                    Runnable startAction = mVpa.mStartAction;
+                    mVpa.mStartAction = null;
+                    startAction.run();
                 }
                 Object listenerTag = view.getTag(LISTENER_TAG_ID);
                 ViewPropertyAnimatorListener listener = null;
@@ -560,7 +564,9 @@
                     // Pre-v16 seems to have a bug where onAnimationEnd is called
                     // twice, therefore we only dispatch on the first call
                     if (mVpa.mEndAction != null) {
-                        mVpa.mEndAction.run();
+                        Runnable endAction = mVpa.mEndAction;
+                        mVpa.mEndAction = null;
+                        endAction.run();
                     }
                     Object listenerTag = view.getTag(LISTENER_TAG_ID);
                     ViewPropertyAnimatorListener listener = null;
diff --git a/v4/tests/AndroidManifest.xml b/v4/tests/AndroidManifest.xml
index 85ffd7d..f0fea8f 100644
--- a/v4/tests/AndroidManifest.xml
+++ b/v4/tests/AndroidManifest.xml
@@ -38,6 +38,8 @@
 
         <activity android:name="android.support.v4.widget.TestActivity"/>
 
+        <activity android:name="android.support.v4.view.VpaActivity"/>
+
         <activity
             android:name="android.support.v4.ThemedYellowActivity"
             android:theme="@style/YellowTheme" />
diff --git a/v4/tests/java/android/support/v4/view/ViewPropertyAnimatorCompatTest.java b/v4/tests/java/android/support/v4/view/ViewPropertyAnimatorCompatTest.java
new file mode 100644
index 0000000..13a78dc
--- /dev/null
+++ b/v4/tests/java/android/support/v4/view/ViewPropertyAnimatorCompatTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 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 android.support.v4.view;
+
+import android.app.Activity;
+import android.support.test.InstrumentationRegistry;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.support.v4.test.R;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ViewPropertyAnimatorCompatTest extends BaseInstrumentationTestCase<VpaActivity> {
+
+    private View mView;
+    private int mVariable;
+    private int mNumListenerCalls = 0;
+
+    public ViewPropertyAnimatorCompatTest() {
+        super(VpaActivity.class);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        final Activity activity = mActivityTestRule.getActivity();
+        mView = activity.findViewById(R.id.view);
+    }
+
+    @Test
+    @MediumTest
+    public void testWithEndAction() throws Throwable {
+        final CountDownLatch latch1 = new CountDownLatch(1);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ViewCompat.animate(mView).alpha(0).setDuration(100).withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        latch1.countDown();
+                    }
+                });
+            }
+        });
+        assertTrue(latch1.await(300, TimeUnit.MILLISECONDS));
+
+        // This test ensures that the endAction listener will be called exactly once
+        mNumListenerCalls = 0;
+        final CountDownLatch latch2 = new CountDownLatch(1);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ViewCompat.animate(mView).alpha(0).setDuration(50).withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        ++mNumListenerCalls;
+                        ViewCompat.animate(mView).alpha(1);
+                        latch2.countDown();
+                    }
+                });
+            }
+        });
+        assertTrue(latch2.await(200, TimeUnit.MILLISECONDS));
+        // Now sleep to allow second listener callback to happen, if it will
+        Thread.sleep(200);
+        assertEquals(1, mNumListenerCalls);
+    }
+
+    @Test
+    @MediumTest
+    public void testWithStartAction() throws Throwable {
+        final CountDownLatch latch1 = new CountDownLatch(1);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ViewCompat.animate(mView).alpha(0).setDuration(100).withStartAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        latch1.countDown();
+                    }
+                });
+            }
+        });
+        assertTrue(latch1.await(100, TimeUnit.MILLISECONDS));
+
+        // This test ensures that the startAction listener will be called exactly once
+        mNumListenerCalls = 0;
+        final CountDownLatch latch2 = new CountDownLatch(1);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ViewCompat.animate(mView).alpha(0).setDuration(50).withStartAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        ++mNumListenerCalls;
+                        ViewCompat.animate(mView).alpha(1);
+                        latch2.countDown();
+                    }
+                });
+            }
+        });
+        assertTrue(latch2.await(200, TimeUnit.MILLISECONDS));
+        // Now sleep to allow second listener callback to happen, if it will
+        Thread.sleep(200);
+        assertEquals(1, mNumListenerCalls);
+    }
+}
diff --git a/v4/tests/java/android/support/v4/view/VpaActivity.java b/v4/tests/java/android/support/v4/view/VpaActivity.java
new file mode 100644
index 0000000..d84d62d
--- /dev/null
+++ b/v4/tests/java/android/support/v4/view/VpaActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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 android.support.v4.view;
+
+import android.app.Activity;
+import android.support.v4.test.R;
+import android.os.Bundle;
+
+public class VpaActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.vpa_activity);
+    }
+}
diff --git a/v4/tests/res/layout/vpa_activity.xml b/v4/tests/res/layout/vpa_activity.xml
new file mode 100644
index 0000000..2363160
--- /dev/null
+++ b/v4/tests/res/layout/vpa_activity.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:id="@+id/content"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent">
+
+    <View
+        android:id="@+id/view"
+        android:background="#f00"
+        android:layout_width="400dp"
+        android:layout_height="200dp"/>
+
+</LinearLayout>