Add local wallpaper color extraction to folder backgrounds.

- Starts listening to local wallpaper colors as soon as folder
  is positioned.
- The folder open animation already has a background color
  animator.
- For simplicity, folder open animation always starts
  first, and then we cancel its background color animator,
  and start a new animator to the extracted color.
- setAutoCancel would be handy here, but it does not work, will
  track in a separate bug.
- Since folders are reused, we clear our the color extractor on
  folder close.

Bug: 175329686
Test: multiple folders on rainbow background
      open each folder, ensure bg animates to proper color
Change-Id: Ie0f2c0632fa2361f7710ba247d74f882620dec16
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 95cdbdd..32b2c9a 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LocalColorExtractor;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -108,12 +109,21 @@
 
     protected boolean mIsOpen;
 
+    // Index used to get background color when using local wallpaper color extraction.
+    protected int mColorExtractionIndex;
+
     public AbstractFloatingView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        init(context);
     }
 
     public AbstractFloatingView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mColorExtractionIndex = LocalColorExtractor.getColorIndex(context);
     }
 
     /**
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index cf1cb45..92d891f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -31,6 +31,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
@@ -38,6 +39,8 @@
 import android.graphics.Insets;
 import android.graphics.Path;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.GradientDrawable;
 import android.os.Build;
 import android.text.InputType;
 import android.text.Selection;
@@ -45,6 +48,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.SparseIntArray;
 import android.util.TypedValue;
 import android.view.FocusFinder;
 import android.view.KeyEvent;
@@ -96,13 +100,16 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.views.ClipPathView;
+import com.android.launcher3.widget.LocalColorExtractor;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -152,6 +159,7 @@
     private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
 
     private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
+    private static final int FOLDER_COLOR_ANIMATION_DURATION = 150;
 
     private static final int REORDER_DELAY = 250;
     private static final int ON_EXIT_CLOSE_DELAY = 400;
@@ -224,6 +232,19 @@
 
     @Nullable private FolderWindowInsetsAnimationCallback mFolderWindowInsetsAnimationCallback;
 
+    // Wallpaper local color extraction
+    @Nullable private LocalColorExtractor mColorExtractor;
+    @Nullable private LocalColorExtractor.Listener mColorListener;
+    private final Rect mTempRect = new Rect();
+    private final RectF mTempRectF = new RectF();
+
+    // For simplicity, we start the color change only after the open animation has started.
+    private Runnable mColorChangeRunnable;
+    private Animator mColorChangeAnimator;
+    // The background color animator used in the folder open animation. We keep a reference to this,
+    // so that we can cancel it when starting mColorChangeAnimator.
+    private ObjectAnimator mOpenAnimationColorChangeAnimator;
+
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -241,7 +262,6 @@
         // name is complete, we have something to focus on, thus hiding the cursor and giving
         // reliable behavior when clicking the text field (since it will always gain focus on click).
         setFocusableInTouchMode(true);
-
     }
 
     @Override
@@ -276,6 +296,45 @@
 
             setWindowInsetsAnimationCallback(mFolderWindowInsetsAnimationCallback);
         }
+
+        if (Utilities.ATLEAST_S) {
+            mColorExtractor = LocalColorExtractor.newInstance(mLauncher);
+            mColorListener = (RectF rect, SparseIntArray extractedColors) -> {
+                mColorChangeRunnable = () -> {
+                    mColorChangeRunnable = null;
+                    int duration = FOLDER_COLOR_ANIMATION_DURATION;
+
+                    // Cancel the open animation color change animator.
+                    ObjectAnimator existingAnim = mOpenAnimationColorChangeAnimator;
+                    if (existingAnim != null && existingAnim.isRunning()) {
+                        duration = (int) Math.max(FOLDER_COLOR_ANIMATION_DURATION,
+                                existingAnim.getDuration() * (1f - existingAnim.getDuration()));
+                        existingAnim.cancel();
+                        mOpenAnimationColorChangeAnimator = null;
+                    }
+
+                    // Start a new animator to the extracted color.
+                    int newColor = extractedColors.get(mColorExtractionIndex);
+                    GradientDrawable bg = (GradientDrawable) getBackground();
+                    mColorChangeAnimator = ObjectAnimator.ofArgb(bg, "color",
+                            bg.getColor().getDefaultColor(), newColor).setDuration(duration);
+                    mColorChangeAnimator.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mColorChangeAnimator = null;
+                        }
+                    });
+                    mColorChangeAnimator.start();
+                };
+
+                // If the folder open animation has started, we can start the color change now.
+                // Otherwise we wait for it to start.
+                if (mOpenAnimationColorChangeAnimator != null
+                        && mOpenAnimationColorChangeAnimator.isStarted()) {
+                    post(mColorChangeRunnable);
+                }
+            };
+        }
     }
 
     public boolean onLongClick(View v) {
@@ -652,15 +711,18 @@
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
-        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
-            mCurrentAnimator.cancel();
-        }
-        AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator();
+        cancelRunningAnimations();
+        FolderAnimationManager fam = new FolderAnimationManager(this, true /* isOpening */);
+        AnimatorSet anim = fam.getAnimator();
+        mOpenAnimationColorChangeAnimator = fam.getBgColorAnimator();
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
                 mFolderIcon.setIconVisible(false);
                 mFolderIcon.drawLeaveBehindIfExists();
+                if (mColorChangeRunnable != null) {
+                    mColorChangeRunnable.run();
+                }
             }
             @Override
             public void onAnimationEnd(Animator animation) {
@@ -752,6 +814,15 @@
                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
     }
 
+    private void cancelRunningAnimations() {
+        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+            mCurrentAnimator.cancel();
+        }
+        if (mColorChangeAnimator != null && mColorChangeAnimator.isRunning()) {
+            mColorChangeAnimator.cancel();
+        }
+    }
+
     private void animateClosed() {
         if (mIsAnimatingClosed) {
             return;
@@ -760,9 +831,7 @@
         mContent.completePendingPageChanges();
         mContent.snapToPageImmediately(mContent.getDestinationPage());
 
-        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
-            mCurrentAnimator.cancel();
-        }
+        cancelRunningAnimations();
         AnimatorSet a = new FolderAnimationManager(this, false /* isOpening */).getAnimator();
         a.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -837,6 +906,19 @@
         clearDragInfo();
         mState = STATE_SMALL;
         mContent.setCurrentPage(0);
+
+        mOpenAnimationColorChangeAnimator = null;
+        mColorChangeRunnable = null;
+        if (mColorChangeAnimator != null) {
+            mColorChangeAnimator.cancel();
+            mColorChangeAnimator = null;
+        }
+        if (mColorExtractor != null) {
+            mColorExtractor.removeLocations();
+            mColorExtractor.setListener(null);
+        }
+        GradientDrawable bg = (GradientDrawable) getBackground();
+        bg.setColor(Themes.getAttrColor(getContext(), R.attr.folderFillColor));
     }
 
     @Override
@@ -1104,6 +1186,17 @@
         lp.height = height;
         lp.x = left;
         lp.y = top;
+
+        if (mColorExtractor != null) {
+            mColorExtractor.removeLocations();
+            mColorExtractor.setListener(mColorListener);
+            mTempRect.set(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
+            mColorExtractor.getExtractedRectForViewRect(mLauncher,
+                    mLauncher.getWorkspace().getCurrentPage(), mTempRect, mTempRectF);
+            if (!mTempRectF.isEmpty()) {
+                mColorExtractor.addLocation(Arrays.asList(mTempRectF));
+            }
+        }
     }
 
     protected int getContentAreaHeight() {
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index ee6ea99..af8be8d 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -35,8 +35,6 @@
 import android.view.View;
 import android.view.animation.AnimationUtils;
 
-import androidx.core.graphics.ColorUtils;
-
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.R;
@@ -80,6 +78,8 @@
     private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
     private final FolderGridOrganizer mPreviewVerifier;
 
+    private ObjectAnimator mBgColorAnimator;
+
     public FolderAnimationManager(Folder folder, boolean isOpening) {
         mFolder = folder;
         mContent = folder.mContent;
@@ -105,6 +105,12 @@
                 R.interpolator.large_folder_preview_item_close_interpolator);
     }
 
+    /**
+     * Returns the animator that changes the background color.
+     */
+    public ObjectAnimator getBgColorAnimator() {
+        return mBgColorAnimator;
+    }
 
     /**
      * Prepares the Folder for animating between open / closed states.
@@ -162,10 +168,15 @@
         final float yDistance = initialY - lp.y;
 
         // Set up the Folder background.
-        final int finalColor = ColorUtils.setAlphaComponent(
-                Themes.getAttrColor(mContext, R.attr.folderFillColor), 255);
-        final int initialColor = setColorAlphaBound(
-                finalColor, mPreviewBackground.getBackgroundAlpha());
+        int previewBackgroundColor = Themes.getAttrColor(mContext, R.attr.folderFillColor);
+        final int finalColor;
+        if (mIsOpening) {
+            finalColor = Themes.getAttrColor(mContext, R.attr.popupColorPrimary);
+        } else {
+            finalColor = mFolderBackground.getColor().getDefaultColor();
+        }
+        final int initialColor = setColorAlphaBound(previewBackgroundColor,
+                mPreviewBackground.getBackgroundAlpha());
         mFolderBackground.mutate();
         mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
 
@@ -193,11 +204,12 @@
             play(a, anim);
         }
 
+        mBgColorAnimator = getAnimator(mFolderBackground, "color", initialColor, finalColor);
+        play(a, mBgColorAnimator);
         play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
         play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
         play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale));
         play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale));
-        play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor));
         play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening));
         play(a, getShape().createRevealAnimator(
                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
@@ -409,7 +421,7 @@
                 : ObjectAnimator.ofFloat(view, property, v2, v1);
     }
 
-    private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
+    private ObjectAnimator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
         return mIsOpening
                 ? ObjectAnimator.ofArgb(drawable, property, v1, v2)
                 : ObjectAnimator.ofArgb(drawable, property, v2, v1);
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index c63d69d..33d8be0 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -79,9 +79,6 @@
 
     // +1 for system shortcut view
     private static final int MAX_NUM_CHILDREN = MAX_SHORTCUTS + 1;
-    // Index used to get background color when using local wallpaper color extraction,
-    private static final int LIGHT_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_50;
-    private static final int DARK_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_800;
 
     private final Rect mTempRect = new Rect();
 
@@ -119,7 +116,6 @@
     private final int[] mColors;
     private final HashMap<String, View> mViewForRect = new HashMap<>();
 
-    private final int mColorExtractionIndex;
     @Nullable private LocalColorExtractor mColorExtractor;
 
     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
@@ -128,9 +124,6 @@
         mOutlineRadius = Themes.getDialogCornerRadius(context);
         mLauncher = BaseDraggingActivity.fromContext(context);
         mIsRtl = Utilities.isRtl(getResources());
-        mColorExtractionIndex = Utilities.isDarkTheme(context)
-                ? DARK_COLOR_EXTRACTION_INDEX
-                : LIGHT_COLOR_EXTRACTION_INDEX;
         setClipToOutline(true);
         setOutlineProvider(new ViewOutlineProvider() {
             @Override
diff --git a/src/com/android/launcher3/widget/LocalColorExtractor.java b/src/com/android/launcher3/widget/LocalColorExtractor.java
index 8ae6b2e..a66024a 100644
--- a/src/com/android/launcher3/widget/LocalColorExtractor.java
+++ b/src/com/android/launcher3/widget/LocalColorExtractor.java
@@ -28,6 +28,7 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.util.ResourceBasedOverride;
 
 import java.util.List;
@@ -35,6 +36,10 @@
 /** Extracts the colors we need from the wallpaper at given locations. */
 public class LocalColorExtractor implements ResourceBasedOverride {
 
+    // Index used to get background color when using local wallpaper color extraction,
+    private static final int LIGHT_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_50;
+    private static final int DARK_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_800;
+
     /** Listener for color changes on a screen location. */
     public interface Listener {
         /**
@@ -103,4 +108,13 @@
             RectF colorExtractionRectOut) {
         // no-op
     }
+
+    /**
+     * Returns an index used to query the color of interest from the list of extracted colors.
+     */
+    public static int getColorIndex(Context context) {
+        return Utilities.isDarkTheme(context)
+                ? DARK_COLOR_EXTRACTION_INDEX
+                : LIGHT_COLOR_EXTRACTION_INDEX;
+    }
 }