Import launcher style reveal animations to QS.

When a new QS tile is added automatically (not through user
customization), the next time the user pulls down QS, reveal this new
tile in the same style as the launcher. Only play this animation once
for each new tile added. This reveal animation will only run if the new
tile is not on the first page in QS.

Bug: 73741556
Test: visual
Change-Id: I8f642d8fd51f63f999eb3f811c13c40f2bea60fa
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index ee573fb..396d317 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -24,6 +24,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Map;
+import java.util.Set;
 
 public final class Prefs {
     private Prefs() {} // no instantation
@@ -87,6 +88,7 @@
         String NUM_APPS_LAUNCHED = "NumAppsLaunched";
         String HAS_SEEN_RECENTS_ONBOARDING = "HasSeenRecentsOnboarding";
         String SEEN_RINGER_GUIDANCE_COUNT = "RingerGuidanceCount";
+        String QS_TILE_SPECS_REVEALED = "QsTileSpecsRevealed";
     }
 
     public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
@@ -121,6 +123,15 @@
         get(context).edit().putString(key, value).apply();
     }
 
+    public static void putStringSet(Context context, @Key String key, Set<String> value) {
+        get(context).edit().putStringSet(key, value).apply();
+    }
+
+    public static Set<String> getStringSet(
+            Context context, @Key String key, Set<String> defaultValue) {
+        return get(context).getStringSet(key, defaultValue);
+    }
+
     public static Map<String, ?> getAll(Context context) {
         return get(context).getAll();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index f3417dc..ea3a60b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -1,5 +1,10 @@
 package com.android.systemui.qs;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -8,20 +13,34 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.Scroller;
 
 import com.android.systemui.R;
 import com.android.systemui.qs.QSPanel.QSTileLayout;
 import com.android.systemui.qs.QSPanel.TileRecord;
 
 import java.util.ArrayList;
+import java.util.Set;
 
 public class PagedTileLayout extends ViewPager implements QSTileLayout {
 
     private static final boolean DEBUG = false;
 
     private static final String TAG = "PagedTileLayout";
+    private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
+    private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
+    private static final long BOUNCE_ANIMATION_DURATION = 450L;
+    private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
+    private static final Interpolator SCROLL_CUBIC = (t) -> {
+        t -= 1.0f;
+        return t * t * t + 1.0f;
+    };
+
 
     private final ArrayList<TileRecord> mTiles = new ArrayList<TileRecord>();
     private final ArrayList<TilePage> mPages = new ArrayList<TilePage>();
@@ -34,37 +53,17 @@
     private int mPosition;
     private boolean mOffPage;
     private boolean mListening;
+    private Scroller mScroller;
+
+    private AnimatorSet mBounceAnimatorSet;
+    private int mAnimatingToPage = -1;
 
     public PagedTileLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mScroller = new Scroller(context, SCROLL_CUBIC);
         setAdapter(mAdapter);
-        setOnPageChangeListener(new OnPageChangeListener() {
-            @Override
-            public void onPageSelected(int position) {
-                if (mPageIndicator == null) return;
-                if (mPageListener != null) {
-                    mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
-                            : position == 0);
-                }
-            }
-
-            @Override
-            public void onPageScrolled(int position, float positionOffset,
-                    int positionOffsetPixels) {
-                if (mPageIndicator == null) return;
-                setCurrentPage(position, positionOffset != 0);
-                mPageIndicator.setLocation(position + positionOffset);
-                if (mPageListener != null) {
-                    mPageListener.onPageChanged(positionOffsetPixels == 0 &&
-                            (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
-                }
-            }
-
-            @Override
-            public void onPageScrollStateChanged(int state) {
-            }
-        });
-        setCurrentItem(0);
+        setOnPageChangeListener(mOnPageChangeListener);
+        setCurrentItem(0, false);
     }
 
     @Override
@@ -99,6 +98,45 @@
         }
     }
 
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        // Suppress all touch event during reveal animation.
+        if (mAnimatingToPage != -1) {
+            return true;
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        // Suppress all touch event during reveal animation.
+        if (mAnimatingToPage != -1) {
+            return true;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public void computeScroll() {
+        if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
+            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
+            float pageFraction = (float) getScrollX() / getWidth();
+            int position = (int) pageFraction;
+            float positionOffset = pageFraction - position;
+            mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX());
+            // Keep on drawing until the animation has finished.
+            postInvalidateOnAnimation();
+            return;
+        }
+        if (mAnimatingToPage != -1) {
+            setCurrentItem(mAnimatingToPage, true);
+            mBounceAnimatorSet.start();
+            setOffscreenPageLimit(1);
+            mAnimatingToPage = -1;
+        }
+        super.computeScroll();
+    }
+
     /**
      * Sets individual pages to listening or not.  If offPage it will set
      * the next page after position to listening as well since we are in between
@@ -257,9 +295,84 @@
         return mPages.get(0).mColumns;
     }
 
+    public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
+        if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) {
+            // Do not start the reveal animation unless there are tiles to animate, multiple
+            // TilePages available and the user has not already started dragging.
+            return;
+        }
+
+        final int lastPageNumber = mPages.size() - 1;
+        final TilePage lastPage = mPages.get(lastPageNumber);
+        final ArrayList<Animator> bounceAnims = new ArrayList<>();
+        for (TileRecord tr : lastPage.mRecords) {
+            if (tileSpecs.contains(tr.tile.getTileSpec())) {
+                bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
+            }
+        }
+
+        if (bounceAnims.isEmpty()) {
+            // All tileSpecs are on the first page. Nothing to do.
+            // TODO: potentially show a bounce animation for first page QS tiles
+            return;
+        }
+
+        mBounceAnimatorSet = new AnimatorSet();
+        mBounceAnimatorSet.playTogether(bounceAnims);
+        mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mBounceAnimatorSet = null;
+                postAnimation.run();
+            }
+        });
+        mAnimatingToPage = lastPageNumber;
+        setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated.
+        mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0,
+                REVEAL_SCROLL_DURATION_MILLIS);
+        postInvalidateOnAnimation();
+    }
+
+    private static Animator setupBounceAnimator(View view, int ordinal) {
+        view.setAlpha(0f);
+        view.setScaleX(0f);
+        view.setScaleY(0f);
+        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
+                PropertyValuesHolder.ofFloat(View.ALPHA, 1),
+                PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
+                PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
+        animator.setDuration(BOUNCE_ANIMATION_DURATION);
+        animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
+        animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
+        return animator;
+    }
+
+    private final ViewPager.OnPageChangeListener mOnPageChangeListener =
+            new ViewPager.SimpleOnPageChangeListener() {
+                @Override
+                public void onPageSelected(int position) {
+                    if (mPageIndicator == null) return;
+                    if (mPageListener != null) {
+                        mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
+                                : position == 0);
+                    }
+                }
+
+                @Override
+                public void onPageScrolled(int position, float positionOffset,
+                        int positionOffsetPixels) {
+                    if (mPageIndicator == null) return;
+                    setCurrentPage(position, positionOffset != 0);
+                    mPageIndicator.setLocation(position + positionOffset);
+                    if (mPageListener != null) {
+                        mPageListener.onPageChanged(positionOffsetPixels == 0 &&
+                                (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
+                    }
+                }
+            };
+
     public static class TilePage extends TileLayout {
         private int mMaxRows = 3;
-
         public TilePage(Context context, AttributeSet attrs) {
             super(context, attrs);
             updateResources();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 5758762..29f3c43 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -290,6 +290,7 @@
         // Let the views animate their contents correctly by giving them the necessary context.
         mHeader.setExpansion(mKeyguardShowing, expansion, panelTranslationY);
         mFooter.setExpansion(mKeyguardShowing ? 1 : expansion);
+        mQSPanel.getQsTileRevealController().setExpansion(expansion);
         mQSPanel.setTranslationY(translationScaleY * heightDiff);
         mQSDetail.setFullyExpanded(fullyExpanded);
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 143ad21..61e3065 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -60,11 +60,12 @@
     public static final String QS_SHOW_HEADER = "qs_show_header";
 
     protected final Context mContext;
-    protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
+    protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
     protected final View mBrightnessView;
     private final H mHandler = new H();
     private final View mPageIndicator;
     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+    private final QSTileRevealController mQsTileRevealController;
 
     protected boolean mExpanded;
     protected boolean mListening;
@@ -108,6 +109,8 @@
         addView(mPageIndicator);
 
         ((PagedTileLayout) mTileLayout).setPageIndicator((PageIndicator) mPageIndicator);
+        mQsTileRevealController = new QSTileRevealController(mContext, this,
+                ((PagedTileLayout) mTileLayout));
 
         addDivider();
 
@@ -136,6 +139,10 @@
         return mPageIndicator;
     }
 
+    public QSTileRevealController getQsTileRevealController() {
+        return mQsTileRevealController;
+    }
+
     public boolean isShowingCustomize() {
         return mCustomizePanel != null && mCustomizePanel.isCustomizing();
     }
@@ -352,6 +359,9 @@
     }
 
     public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
+        if (!collapsedView) {
+            mQsTileRevealController.updateRevealedTiles(tiles);
+        }
         for (TileRecord record : mRecords) {
             mTileLayout.removeTile(record);
             record.tile.removeCallback(record.callback);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java
new file mode 100644
index 0000000..2f012e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java
@@ -0,0 +1,76 @@
+package com.android.systemui.qs;
+
+import static com.android.systemui.Prefs.Key.QS_TILE_SPECS_REVEALED;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.ArraySet;
+
+import com.android.systemui.Prefs;
+import com.android.systemui.plugins.qs.QSTile;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+public class QSTileRevealController {
+    private static final long QS_REVEAL_TILES_DELAY = 500L;
+
+    private final Context mContext;
+    private final QSPanel mQSPanel;
+    private final PagedTileLayout mPagedTileLayout;
+    private final ArraySet<String> mTilesToReveal = new ArraySet<>();
+    private final Handler mHandler = new Handler();
+
+    private final Runnable mRevealQsTiles = new Runnable() {
+        @Override
+        public void run() {
+            mPagedTileLayout.startTileReveal(mTilesToReveal, () -> {
+                if (mQSPanel.isExpanded()) {
+                    addTileSpecsToRevealed(mTilesToReveal);
+                    mTilesToReveal.clear();
+                }
+            });
+        }
+    };
+
+    QSTileRevealController(Context context, QSPanel qsPanel, PagedTileLayout pagedTileLayout) {
+        mContext = context;
+        mQSPanel = qsPanel;
+        mPagedTileLayout = pagedTileLayout;
+    }
+
+    public void setExpansion(float expansion) {
+        if (expansion == 1f) {
+            mHandler.postDelayed(mRevealQsTiles, QS_REVEAL_TILES_DELAY);
+        } else {
+            mHandler.removeCallbacks(mRevealQsTiles);
+        }
+    }
+
+    public void updateRevealedTiles(Collection<QSTile> tiles) {
+        ArraySet<String> tileSpecs = new ArraySet<>();
+        for (QSTile tile : tiles) {
+            tileSpecs.add(tile.getTileSpec());
+        }
+
+        final Set<String> revealedTiles = Prefs.getStringSet(
+                mContext, QS_TILE_SPECS_REVEALED, Collections.EMPTY_SET);
+        if (revealedTiles.isEmpty() || mQSPanel.isShowingCustomize()) {
+            // Do not reveal QS tiles the user has upon first load or those that they directly
+            // added through customization.
+            addTileSpecsToRevealed(tileSpecs);
+        } else {
+            // Animate all tiles that the user has not directly added themselves.
+            tileSpecs.removeAll(revealedTiles);
+            mTilesToReveal.addAll(tileSpecs);
+        }
+    }
+
+    private void addTileSpecsToRevealed(ArraySet<String> specs) {
+        final ArraySet<String> revealedTiles = new ArraySet<>(
+                Prefs.getStringSet(mContext, QS_TILE_SPECS_REVEALED, Collections.EMPTY_SET));
+        revealedTiles.addAll(specs);
+        Prefs.putStringSet(mContext, QS_TILE_SPECS_REVEALED, revealedTiles);
+    }
+}