ImageCardView: switch to use ObjectAnimator.

In this CL, we have switched the usage of view.animate().alpha() to ObjectAnimator.

A test case was also added to provide a thorough testintg on the
setMainImage(Drawable drawable, boolean fade) method which can
trigger animation's execution directly.

Bug: 64299355

Test: ImageCardViewTest

Change-Id: I361347607a871a77d825c56e4b35d8c7b6c197c5
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
index 8f0c66e..53b27c6 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
@@ -13,6 +13,7 @@
  */
 package android.support.v17.leanback.widget;
 
+import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
@@ -122,12 +123,15 @@
     public static final int CARD_TYPE_FLAG_ICON_RIGHT = 4;
     public static final int CARD_TYPE_FLAG_ICON_LEFT = 8;
 
+    private static final String ALPHA = "alpha";
+
     private ImageView mImageView;
     private ViewGroup mInfoArea;
     private TextView mTitleView;
     private TextView mContentView;
     private ImageView mBadgeImage;
     private boolean mAttachedToWindow;
+    ObjectAnimator mFadeInAnimator;
 
     /**
      * Create an ImageCardView using a given theme for customization.
@@ -179,6 +183,10 @@
         if (mImageView.getDrawable() == null) {
             mImageView.setVisibility(View.INVISIBLE);
         }
+        // Set Object Animator for image view.
+        mFadeInAnimator = ObjectAnimator.ofFloat(mImageView, ALPHA, 1f);
+        mFadeInAnimator.setDuration(
+                mImageView.getResources().getInteger(android.R.integer.config_shortAnimTime));
 
         mInfoArea = findViewById(R.id.info_field);
         if (hasImageOnly) {
@@ -324,7 +332,7 @@
 
         mImageView.setImageDrawable(drawable);
         if (drawable == null) {
-            mImageView.animate().cancel();
+            mFadeInAnimator.cancel();
             mImageView.setAlpha(1f);
             mImageView.setVisibility(View.INVISIBLE);
         } else {
@@ -332,7 +340,7 @@
             if (fade) {
                 fadeIn();
             } else {
-                mImageView.animate().cancel();
+                mFadeInAnimator.cancel();
                 mImageView.setAlpha(1f);
             }
         }
@@ -458,8 +466,7 @@
     private void fadeIn() {
         mImageView.setAlpha(0f);
         if (mAttachedToWindow) {
-            mImageView.animate().alpha(1f).setDuration(
-                    mImageView.getResources().getInteger(android.R.integer.config_shortAnimTime));
+            mFadeInAnimator.start();
         }
     }
 
@@ -480,9 +487,8 @@
     @Override
     protected void onDetachedFromWindow() {
         mAttachedToWindow = false;
-        mImageView.animate().cancel();
+        mFadeInAnimator.cancel();
         mImageView.setAlpha(1f);
         super.onDetachedFromWindow();
     }
-
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
new file mode 100644
index 0000000..a407e28
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2017 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.v17.leanback.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.app.TestActivity;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.View;
+import android.widget.ImageView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.util.Random;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ImageCardViewTest {
+
+    private static final String IMAGE_CARD_VIEW_ACTIVITY = "ImageCardViewActivity";
+    private static final float FINAL_ALPHA_STATE = 1.0f;
+    private static final float INITIAL_ALPHA_STATE = 0.0f;
+    private static final float DELTA = 0.0f;
+    private static final long ANIMATION_DURATION = 5000;
+    private static final int RANDOM_COLOR_ONE = 0xffffffff;
+    private static final int RANDOM_COLOR_TWO = 0x00000000;
+
+    @Rule
+    public TestName mUnitTestName = new TestName();
+
+    // Enable lifecycle based testing
+    private TestActivity.TestActivityTestRule mRule;
+
+    // Only support alpha animation
+    private static final String ALPHA = "alpha";
+
+    // Flag to represent if the callback has been called or not
+    private boolean mOnAnimationStartCalled;
+    private boolean mOnAnimationPauseCalled;
+    private boolean mOnAnimationResumeCalled;
+    private boolean mOnAnimationCancelCalled;
+    private boolean mOnAnimationEndCalled;
+    private boolean mOnAnimationRepeatCalled;
+
+    // ImageCardView for testing.
+    private ImageCardView mImageCardView;
+
+    // Animator for testing.
+    private ObjectAnimator mFadeInAnimator;
+
+    // ImageView on ImageCardView;
+    private ImageView mImageView;
+
+    // Sample Drawable which will be used as the parameter for some methods.
+    private Drawable mSampleDrawable;
+
+    // Another Sample Drawable.
+    private Drawable mSampleDrawable2;
+
+    // Generated Image View Id.
+    private int mImageCardViewId;
+
+    // Listener to capture animator's state
+    private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationStart(Animator animation) {
+            super.onAnimationStart(animation);
+            mOnAnimationStartCalled = true;
+        }
+
+        @Override
+        public void onAnimationPause(Animator animation) {
+            super.onAnimationPause(animation);
+            mOnAnimationPauseCalled = true;
+        }
+
+        @Override
+        public void onAnimationResume(Animator animation) {
+            super.onAnimationResume(animation);
+            mOnAnimationResumeCalled = true; }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            super.onAnimationCancel(animation);
+            mOnAnimationCancelCalled = true;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            super.onAnimationEnd(animation);
+            mOnAnimationEndCalled = true;
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+            super.onAnimationRepeat(animation);
+            mOnAnimationRepeatCalled = true;
+        }
+    };
+
+    // Set up before executing test cases.
+    @Before
+    public void setUp() throws Exception {
+        // The following provider will create an Activity which can inflate the ImageCardView
+        // And the ImageCardView can be fetched through ID for future testing.
+        TestActivity.Provider imageCardViewProvider = new TestActivity.Provider() {
+            @Override
+            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+                super.onCreate(activity, savedInstanceState);
+
+                // The theme must be set to make sure imageCardView can be populated correctly.
+                activity.setTheme(R.style.Widget_Leanback_ImageCardView_BadgeStyle);
+
+                // Create Drawable using random color for test purpose.
+                mSampleDrawable = new ColorDrawable(RANDOM_COLOR_ONE);
+
+                // Create Drawable using random color for test purpose.
+                mSampleDrawable2 = new ColorDrawable(RANDOM_COLOR_TWO);
+
+                // Create imageCardView and save system generated ID.
+                ImageCardView imageCardView = new ImageCardView(activity);
+                mImageCardViewId = imageCardView.generateViewId();
+                imageCardView.setId(mImageCardViewId);
+
+                // Set up imageCardView with activity programmatically.
+                activity.setContentView(imageCardView);
+            }
+        };
+
+        // Initialize testing rule and testing activity
+        mRule = new TestActivity.TestActivityTestRule(imageCardViewProvider, generateProviderName(
+                IMAGE_CARD_VIEW_ACTIVITY));
+        final TestActivity imageCardViewTestActivity = mRule.launchActivity();
+
+        // Create card view and image view
+        mImageCardView = (ImageCardView) imageCardViewTestActivity.findViewById(mImageCardViewId);
+        mImageView = mImageCardView.getMainImageView();
+
+        // Create animator.
+        mFadeInAnimator = mImageCardView.mFadeInAnimator;
+        mFadeInAnimator.addListener(mAnimatorListener);
+
+        // Set animation duration with longer period of time for robust testing.
+        mFadeInAnimator.setDuration(ANIMATION_DURATION);
+    }
+
+    /**
+     * Test SetMainImage method when the parameters are null and false
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageTest0() throws Throwable {
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(null, false);
+
+                // Currently, the animation hasn't started yet, the cancel event will not be
+                // triggered
+                assertFalse(mOnAnimationCancelCalled);
+
+                // The animation will not be started, check status immediately.
+                assertEquals(mImageCardView.getMainImage(), null);
+                assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+                assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Test SetMainImage method when the parameters are mSampleDrawable and false
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageTest1() throws Throwable {
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(mSampleDrawable, false);
+
+                // Currently, the animation hasn't started yet, the cancel event will not be
+                // triggered
+                assertFalse(mOnAnimationCancelCalled);
+
+                // The animation will not be started, check status immediately.
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+                assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Test SetMainImage method when the parameters are null and true
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageTest2() throws Throwable {
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(null, true);
+
+                // Currently, the animation hasn't started yet, the cancel event will not be
+                // triggered
+                assertFalse(mOnAnimationCancelCalled);
+
+                // The animation will not be started, check status immediately.
+                assertEquals(mImageCardView.getMainImage(), null);
+                assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+                assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Test SetMainImage method with sample drawable object and true parameter
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageTest3() throws Throwable {
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(mSampleDrawable, true);
+
+                // The fadeIn method should be triggered in this scenario
+                assertTrue(mOnAnimationStartCalled);
+
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+
+        // Set time out limitation to be 2 * ANIMATION_DURATION.
+        PollingCheck.waitFor(2 * ANIMATION_DURATION, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mOnAnimationEndCalled;
+            }
+        });
+
+        // Test if animation ended successfully through alpha value.
+        assertTrue(mOnAnimationEndCalled);
+        assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+    }
+
+    /**
+     * Test SetMainImage method's behavior when the animation is already started
+     * In this test case, the parameters are set to null and false to interrupt existed animation
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageInTransitionTest0() throws Throwable {
+        // The transition duration before the interruption happens.
+        long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+        // Perform an animation firstly
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(mSampleDrawable, true);
+
+                // The fadeIn method should be triggered in this scenario
+                assertTrue(mOnAnimationStartCalled);
+
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+
+        // simulate the duration of animation
+        SystemClock.sleep(durationBeforeInterruption);
+
+        // Interrupt current animation using setMainImage(Drawable, boolean) method
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // Interrupt existed animation
+                mImageCardView.setMainImage(null, false);
+
+                // Existed animation will be cancelled immediately.
+                assertTrue(mOnAnimationCancelCalled);
+
+                // New animation will not be triggered, check the status immediately
+                assertEquals(mImageCardView.getMainImage(), null);
+                assertEquals(mImageCardView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+                assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Test SetMainImage method's behavior when the animation is already started
+     * In this test case, the parameters are set to mSampleDrawable2 and false to interrupt
+     * existed animation
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageInTransitionTest1() throws Throwable {
+        // The transition duration before the interruption happens.
+        long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+        // Perform an animation firstly
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(mSampleDrawable, true);
+
+                // The fadeIn method should be triggered in this scenario
+                assertTrue(mOnAnimationStartCalled);
+
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+
+        // simulate the duration of animation
+        SystemClock.sleep(durationBeforeInterruption);
+
+        // Interrupt current animation using setMainImage(Drawable, boolean) method
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // Interrupt existed animation
+                mImageCardView.setMainImage(mSampleDrawable2, false);
+
+                // Existed animation will be cancelled immediately.
+                assertTrue(mOnAnimationCancelCalled);
+
+                // New animation will not be triggered, check the status immediately
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable2);
+                assertEquals(mImageCardView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Test SetMainImage method's behavior when the animation is already started
+     * In this test case, the parameters are set to null and true to interrupt existed animation
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageInTransitionTest2() throws Throwable {
+        // The transition duration before the interruption happens.
+        long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+        // Perform an animation firstly
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(mSampleDrawable, true);
+
+                // The fadeIn method should be triggered in this scenario
+                assertTrue(mOnAnimationStartCalled);
+
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+
+        // simulate the duration of animation
+        SystemClock.sleep(durationBeforeInterruption);
+
+        // Interrupt current animation using setMainImage(Drawable, boolean) method
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // Interrupt existed animation
+                mImageCardView.setMainImage(null, true);
+
+                // Existed animation will be cancelled immediately.
+                assertTrue(mOnAnimationCancelCalled);
+
+                // New animation will not be triggered, check the status immediately
+                assertEquals(mImageCardView.getMainImage(), null);
+                assertEquals(mImageCardView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+                assertEquals(mImageView.getVisibility(), View.INVISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Test SetMainImage method's behavior when the animation is already started
+     * In this test case, the parameters are set to mSampleDrawable2 and true to interrupt
+     * existed animation
+     *
+     * @throws Throwable
+     */
+    @Test
+    public void testSetMainImageInTransitionTest3() throws Throwable {
+        // The transition duration before the interruption happens.
+        long durationBeforeInterruption = (long) (0.5 * ANIMATION_DURATION);
+
+        // Perform an animation firstly
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // set random alpha value initially
+                mImageView.setAlpha(generateInitialialAlphaValue());
+                mImageCardView.setMainImage(mSampleDrawable, true);
+
+                // The fadeIn method should be triggered in this scenario
+                assertTrue(mOnAnimationStartCalled);
+
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+
+        // Simulate the duration of animation
+        SystemClock.sleep(durationBeforeInterruption);
+
+        // Interrupt current animation using setMainImage(Drawable, boolean) method
+        mRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // Interrupt existed animation
+                mImageCardView.setMainImage(mSampleDrawable2, true);
+
+                // Existed animation will not be cancelled immediately.
+                assertFalse(mOnAnimationCancelCalled);
+
+                // New animation will not be triggered, check the status immediately
+                assertEquals(mImageCardView.getMainImage(), mSampleDrawable2);
+                assertEquals(mImageView.getVisibility(), View.VISIBLE);
+            }
+        });
+
+        // Set time out limitation to be 2 * ANIMATION_DURATION.
+        PollingCheck.waitFor(2 * ANIMATION_DURATION, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mOnAnimationEndCalled;
+            }
+        });
+
+        // Test if animation ended successfully through alpha value.
+        assertEquals(mImageView.getAlpha(), FINAL_ALPHA_STATE, DELTA);
+    }
+
+
+    // Helper function to register provider's name
+    private String generateProviderName(String name) {
+        return mUnitTestName.getMethodName() + "_" + name;
+    }
+
+    // generate random number as the initial alpha value
+    private float generateInitialialAlphaValue() {
+        Random generator = new Random();
+        return generator.nextFloat();
+    }
+}
+
+