Merge changes from topic "tr-anim" into sc-dev

* changes:
  Translate: Text alpha animation on toggling translated state.
  Translate: Ignore duplicate translations.
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index 4b2c343..bf2af51 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -389,6 +389,14 @@
                     continue;
                 }
                 mActivity.runOnUiThread(() -> {
+                    if (view.getViewTranslationResponse() != null
+                            && view.getViewTranslationResponse().equals(response)) {
+                        if (DEBUG) {
+                            Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
+                                    + ". Ignoring.");
+                        }
+                        return;
+                    }
                     ViewTranslationCallback callback = view.getViewTranslationCallback();
                     if (callback == null) {
                         if (view instanceof TextView) {
@@ -396,9 +404,6 @@
                             // implememtation.
                             callback = new TextViewTranslationCallback();
                             view.setViewTranslationCallback(callback);
-                            if (mViewsToPadContent.contains(autofillId)) {
-                                callback.enableContentPadding();
-                            }
                         } else {
                             if (DEBUG) {
                                 Log.d(TAG, view + " doesn't support showing translation because of "
@@ -407,6 +412,10 @@
                             return;
                         }
                     }
+                    callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS);
+                    if (mViewsToPadContent.contains(autofillId)) {
+                        callback.enableContentPadding();
+                    }
                     view.onViewTranslationResponse(response);
                     callback.onShowTranslation(view);
                 });
@@ -414,6 +423,9 @@
         }
     }
 
+    // TODO: Use a device config value.
+    private static final int ANIMATION_DURATION_MILLIS = 250;
+
     /**
      * Creates a Translator for the given source and target translation specs and start the ui
      * translation when the Translator is created successfully.
diff --git a/core/java/android/view/translation/ViewTranslationCallback.java b/core/java/android/view/translation/ViewTranslationCallback.java
index 1f0723b..6efd621 100644
--- a/core/java/android/view/translation/ViewTranslationCallback.java
+++ b/core/java/android/view/translation/ViewTranslationCallback.java
@@ -64,4 +64,12 @@
      * @hide
      */
     default void enableContentPadding() {}
+
+    /**
+     * Sets the duration for animations while transitioning the view between the original and
+     * translated contents.
+     *
+     * @hide
+     */
+    default void setAnimationDurationMillis(int durationMillis) {}
 }
diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java
index 92c9142..a7d5ee4 100644
--- a/core/java/android/widget/TextViewTranslationCallback.java
+++ b/core/java/android/widget/TextViewTranslationCallback.java
@@ -16,10 +16,15 @@
 
 package android.widget;
 
+import android.animation.Animator;
+import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
 import android.os.Build;
 import android.text.TextUtils;
+import android.text.method.TransformationMethod;
 import android.text.method.TranslationTransformationMethod;
 import android.util.Log;
 import android.view.View;
@@ -47,6 +52,7 @@
     private boolean mIsShowingTranslation = false;
     private boolean mIsTextPaddingEnabled = false;
     private CharSequence mPaddedText;
+    private int mAnimationDurationMillis = 250; // default value
 
     private CharSequence mContentDescription;
 
@@ -82,14 +88,19 @@
      */
     @Override
     public boolean onShowTranslation(@NonNull View view) {
-        mIsShowingTranslation = true;
         if (view.getViewTranslationResponse() == null) {
             Log.wtf(TAG, "onShowTranslation() shouldn't be called before "
                     + "onViewTranslationResponse().");
             return false;
         }
         if (mTranslationTransformation != null) {
-            ((TextView) view).setTransformationMethod(mTranslationTransformation);
+            final TransformationMethod transformation = mTranslationTransformation;
+            runWithAnimation(
+                    (TextView) view,
+                    () -> {
+                        mIsShowingTranslation = true;
+                        ((TextView) view).setTransformationMethod(transformation);
+                    });
             ViewTranslationResponse response = view.getViewTranslationResponse();
             if (response.getKeys().contains(ViewTranslationRequest.ID_CONTENT_DESCRIPTION)) {
                 CharSequence translatedContentDescription =
@@ -114,7 +125,6 @@
      */
     @Override
     public boolean onHideTranslation(@NonNull View view) {
-        mIsShowingTranslation = false;
         if (view.getViewTranslationResponse() == null) {
             Log.wtf(TAG, "onHideTranslation() shouldn't be called before "
                     + "onViewTranslationResponse().");
@@ -122,8 +132,14 @@
         }
         // Restore to original text content.
         if (mTranslationTransformation != null) {
-            ((TextView) view).setTransformationMethod(
-                    mTranslationTransformation.getOriginalTransformationMethod());
+            final TransformationMethod transformation =
+                    mTranslationTransformation.getOriginalTransformationMethod();
+            runWithAnimation(
+                    (TextView) view,
+                    () -> {
+                        mIsShowingTranslation = false;
+                        ((TextView) view).setTransformationMethod(transformation);
+                    });
             if (!TextUtils.isEmpty(mContentDescription)) {
                 view.setContentDescription(mContentDescription);
             }
@@ -212,4 +228,64 @@
     }
 
     private static final char COMPAT_PAD_CHARACTER = '\u2002';
+
+    @Override
+    public void setAnimationDurationMillis(int durationMillis) {
+        mAnimationDurationMillis = durationMillis;
+    }
+
+    /**
+     * Applies a simple text alpha animation when toggling between original and translated text. The
+     * text is fully faded out, then swapped to the new text, then the fading is reversed.
+     *
+     * @param runnable the operation to run on the view after the text is faded out, to change to
+     * displaying the original or translated text.
+     */
+    private void runWithAnimation(TextView view, Runnable runnable) {
+        if (mAnimator != null) {
+            mAnimator.end();
+            // Note: mAnimator is now null; do not use again here.
+        }
+        int fadedOutColor = colorWithAlpha(view.getCurrentTextColor(), 0);
+        mAnimator = ValueAnimator.ofArgb(view.getCurrentTextColor(), fadedOutColor);
+        mAnimator.addUpdateListener(
+                // Note that if the text has a ColorStateList, this replaces it with a single color
+                // for all states. The original ColorStateList is restored when the animation ends
+                // (see below).
+                (valueAnimator) -> view.setTextColor((Integer) valueAnimator.getAnimatedValue()));
+        mAnimator.setRepeatMode(ValueAnimator.REVERSE);
+        mAnimator.setRepeatCount(1);
+        mAnimator.setDuration(mAnimationDurationMillis);
+        final ColorStateList originalColors = view.getTextColors();
+        mAnimator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setTextColor(originalColors);
+                mAnimator = null;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+                runnable.run();
+            }
+        });
+        mAnimator.start();
+    }
+
+    private ValueAnimator mAnimator;
+
+    /**
+     * Returns {@code color} with alpha changed to {@code newAlpha}
+     */
+    private static int colorWithAlpha(int color, int newAlpha) {
+        return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
+    }
 }