Adding a compat implementation for playback control on AnimatorSet

Change-Id: I9f01fc319341cf2499fffb59521d32c2c81eba45
diff --git a/src/com/android/launcher3/PinchToOverviewListener.java b/src/com/android/launcher3/PinchToOverviewListener.java
index fc75fe3..d1a2538 100644
--- a/src/com/android/launcher3/PinchToOverviewListener.java
+++ b/src/com/android/launcher3/PinchToOverviewListener.java
@@ -16,30 +16,22 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 
 import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.util.Range;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.ScaleGestureDetector.OnScaleGestureListener;
 
+import com.android.launcher3.compat.AnimatorSetCompat;
 import com.android.launcher3.util.TouchController;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
 /**
  * Detects pinches and animates the Workspace to/from overview mode.
  */
-@TargetApi(Build.VERSION_CODES.O)
 public class PinchToOverviewListener
         implements TouchController, OnScaleGestureListener, Runnable {
 
@@ -55,9 +47,8 @@
     private Workspace mWorkspace = null;
     private boolean mPinchStarted = false;
 
-    private AnimatorSet mCurrentAnimation;
+    private AnimatorSetCompat mCurrentAnimation;
     private float mCurrentScale;
-    private Range<Integer> mDurationRange;
     private boolean mShouldGoToFinalState;
 
     private LauncherState mToState;
@@ -109,14 +100,13 @@
         }
 
         mToState = mLauncher.isInState(OVERVIEW) ? NORMAL : OVERVIEW;
-        mCurrentAnimation = mLauncher.getStateManager()
-                .createAnimationToNewWorkspace(mToState, this);
+        mCurrentAnimation = AnimatorSetCompat.wrap(mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(mToState, this), OVERVIEW_TRANSITION_MS);
         mPinchStarted = true;
         mCurrentScale = 1;
-        mDurationRange = Range.create(0, LauncherAnimUtils.OVERVIEW_TRANSITION_MS);
         mShouldGoToFinalState = false;
 
-        dispatchOnStart(mCurrentAnimation);
+        mCurrentAnimation.dispatchOnStart();
         return true;
     }
 
@@ -160,26 +150,7 @@
         }
 
         // Move the transition animation to that duration.
-        long playPosition = mDurationRange.clamp(
-                (int) ((1 - animationFraction) * mDurationRange.getUpper()));
-        mCurrentAnimation.setCurrentPlayTime(playPosition);
-
+        mCurrentAnimation.setPlayFraction(1 - animationFraction);
         return true;
     }
-
-    private void dispatchOnStart(Animator animator) {
-        for (AnimatorListener l : nonNullList(animator.getListeners())) {
-            l.onAnimationStart(animator);
-        }
-
-        if (animator instanceof AnimatorSet) {
-            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                dispatchOnStart(anim);
-            }
-        }
-    }
-
-    private static <T> List<T> nonNullList(ArrayList<T> list) {
-        return list == null ? Collections.<T>emptyList() : list;
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/compat/AnimatorSetCompat.java b/src/com/android/launcher3/compat/AnimatorSetCompat.java
new file mode 100644
index 0000000..497dd14
--- /dev/null
+++ b/src/com/android/launcher3/compat/AnimatorSetCompat.java
@@ -0,0 +1,204 @@
+/*
+ * 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 com.android.launcher3.compat;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.animation.LinearInterpolator;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Compat implementation for various new APIs in {@link AnimatorSet}
+ *
+ * Note: The compat implementation does not support start delays on child animations or
+ * sequential playbacks.
+ */
+public abstract class AnimatorSetCompat implements ValueAnimator.AnimatorUpdateListener {
+
+    public static AnimatorSetCompat wrap(AnimatorSet anim, int duration) {
+        if (Utilities.ATLEAST_OREO) {
+            return new AnimatorSetCompatVO(anim, duration);
+        } else {
+            return new AnimatorSetCompatVL(anim, duration);
+        }
+    }
+
+    private final ValueAnimator mAnimationPlayer;
+    private final long mDuration;
+
+    protected final AnimatorSet mAnim;
+
+    protected float mCurrentFraction;
+
+    protected AnimatorSetCompat(AnimatorSet anim, int duration) {
+        mAnim = anim;
+        mDuration = duration;
+
+        mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
+        mAnimationPlayer.setInterpolator(new LinearInterpolator());
+        mAnimationPlayer.addUpdateListener(this);
+    }
+
+    /**
+     * Starts playing the animation forward from current position.
+     */
+    public void start() {
+        mAnimationPlayer.setFloatValues(mCurrentFraction, 1);
+        mAnimationPlayer.setDuration(clampDuration(1 - mCurrentFraction));
+        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
+        mAnimationPlayer.start();
+    }
+
+    /**
+     * Starts playing the animation backwards from current position
+     */
+    public void reverse() {
+        mAnimationPlayer.setFloatValues(mCurrentFraction, 0);
+        mAnimationPlayer.setDuration(clampDuration(mCurrentFraction));
+        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
+        mAnimationPlayer.start();
+    }
+
+    /**
+     * Sets the current animation position and updates all the child animators accordingly.
+     */
+    public abstract void setPlayFraction(float fraction);
+
+    /**
+     * @see Animator#addListener(AnimatorListener)
+     */
+    public void addListener(Animator.AnimatorListener listener) {
+        mAnimationPlayer.addListener(listener);
+    }
+
+    @Override
+    public void onAnimationUpdate(ValueAnimator valueAnimator) {
+        setPlayFraction((float) valueAnimator.getAnimatedValue());
+    }
+
+    protected long clampDuration(float fraction) {
+        float playPos = mDuration * fraction;
+        if (playPos <= 0) {
+            return 0;
+        } else {
+            return Math.min((long) playPos, mDuration);
+        }
+    }
+
+    public void dispatchOnStart() {
+        dispatchOnStartRecursively(mAnim);
+    }
+
+    private void dispatchOnStartRecursively(Animator animator) {
+        for (AnimatorListener l : nonNullList(animator.getListeners())) {
+            l.onAnimationStart(animator);
+        }
+
+        if (animator instanceof AnimatorSet) {
+            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
+                dispatchOnStartRecursively(anim);
+            }
+        }
+    }
+
+    public static class AnimatorSetCompatVL extends AnimatorSetCompat {
+
+        private final ValueAnimator[] mChildAnimations;
+
+        private AnimatorSetCompatVL(AnimatorSet anim, int duration) {
+            super(anim, duration);
+
+            // Build animation list
+            ArrayList<ValueAnimator> childAnims = new ArrayList<>();
+            getAnimationsRecur(mAnim, childAnims);
+            mChildAnimations = childAnims.toArray(new ValueAnimator[childAnims.size()]);
+        }
+
+        private void getAnimationsRecur(AnimatorSet anim, ArrayList<ValueAnimator> out) {
+            long forceDuration = anim.getDuration();
+            for (Animator child : anim.getChildAnimations()) {
+                if (forceDuration > 0) {
+                    child.setDuration(forceDuration);
+                }
+                if (child instanceof ValueAnimator) {
+                    out.add((ValueAnimator) child);
+                } else if (child instanceof AnimatorSet) {
+                    getAnimationsRecur((AnimatorSet) child, out);
+                } else {
+                    throw new RuntimeException("Unknown animation type " + child);
+                }
+            }
+        }
+
+        @Override
+        public void setPlayFraction(float fraction) {
+            mCurrentFraction = fraction;
+            long playPos = clampDuration(fraction);
+            for (ValueAnimator anim : mChildAnimations) {
+                anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration()));
+            }
+        }
+
+    }
+
+    @TargetApi(Build.VERSION_CODES.O)
+    private static class AnimatorSetCompatVO extends AnimatorSetCompat {
+
+        private AnimatorSetCompatVO(AnimatorSet anim, int duration) {
+            super(anim, duration);
+        }
+
+        @Override
+        public void setPlayFraction(float fraction) {
+            mCurrentFraction = fraction;
+            mAnim.setCurrentPlayTime(clampDuration(fraction));
+        }
+    }
+
+    private class OnAnimationEndDispatcher extends AnimationSuccessListener {
+
+        @Override
+        public void onAnimationSuccess(Animator animator) {
+            dispatchOnEndRecursively(mAnim);
+        }
+
+        private void dispatchOnEndRecursively(Animator animator) {
+            for (AnimatorListener l : nonNullList(animator.getListeners())) {
+                l.onAnimationEnd(animator);
+            }
+
+            if (animator instanceof AnimatorSet) {
+                for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
+                    dispatchOnEndRecursively(anim);
+                }
+            }
+        }
+    }
+
+    private static <T> List<T> nonNullList(ArrayList<T> list) {
+        return list == null ? Collections.<T>emptyList() : list;
+    }
+}
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index f2bad6b..bc5aafc 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -143,7 +143,7 @@
 
     public void onAccessibilityStateChanged(boolean isAccessibilityEnabled) {
         mPinchListener = FeatureFlags.LAUNCHER3_DISABLE_PINCH_TO_OVERVIEW || isAccessibilityEnabled
-                || !Utilities.ATLEAST_OREO ? null : new PinchToOverviewListener(mLauncher);
+                ? null : new PinchToOverviewListener(mLauncher);
     }
 
     public boolean isEventOverHotseat(MotionEvent ev) {