Make all widgets collapsed in the full widget picker by default

Changes:
1. Add a WidgetListHeader view for showing icon, app name and a subtitle.
2. Only WidgetListHeaders are always visible to users in the full widget
   picker.
3. Only one widgets list from an app is visible in the full widget picker
   at any one time.

Test: Auto: run add robolectric tests under widget/picker
      Manual: Open full widgets picker. Then, expand and collapse apps.
      Video: https://drive.google.com/file/d/1gzfeEm5IOAu0qHsO77OTS2eMfU7CHJiL/view?usp=sharing

Bug: 179797520
Change-Id: Idac58be23dfeafcb79b3c61b4972d3addb462de1
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 21297c9..cea8cd6 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -24,7 +24,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
@@ -34,8 +33,6 @@
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.PointF;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -52,7 +49,6 @@
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
-import androidx.core.graphics.ColorUtils;
 
 import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
@@ -798,7 +794,7 @@
         if (mIcon != null
                 && mIcon instanceof PlaceHolderIconDrawable
                 && iconUpdateAnimationEnabled()) {
-            animateIconUpdate((PlaceHolderIconDrawable) mIcon, icon);
+            ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
         }
 
         mDisableRelayout = false;
@@ -950,28 +946,6 @@
         }
     }
 
-    private static void animateIconUpdate(PlaceHolderIconDrawable oldIcon, Drawable newIcon) {
-        int placeholderColor = oldIcon.mPaint.getColor();
-        int originalAlpha = Color.alpha(placeholderColor);
-
-        ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
-        iconUpdateAnimation.setDuration(ICON_UPDATE_ANIMATION_DURATION);
-        iconUpdateAnimation.addUpdateListener(valueAnimator -> {
-            int newAlpha = (int) valueAnimator.getAnimatedValue();
-            int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
-
-            newIcon.setColorFilter(new PorterDuffColorFilter(newColor, Mode.SRC_ATOP));
-        });
-        iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                newIcon.setColorFilter(null);
-            }
-        });
-        iconUpdateAnimation.start();
-    }
-
-
     @Override
     public void decorate(int color) {
         mHighlightColor = color;
diff --git a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
index d347e8f..b6d25c4 100644
--- a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
@@ -19,10 +19,19 @@
 
 import static com.android.launcher3.graphics.IconShape.getShapePath;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.core.graphics.ColorUtils;
 
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
@@ -53,4 +62,27 @@
         canvas.drawPath(mProgressPath, mPaint);
         canvas.restoreToCount(saveCount);
     }
+
+    /** Updates this placeholder to {@code newIcon} with animation. */
+    public void animateIconUpdate(Drawable newIcon) {
+        int placeholderColor = mPaint.getColor();
+        int originalAlpha = Color.alpha(placeholderColor);
+
+        ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
+        iconUpdateAnimation.setDuration(375);
+        iconUpdateAnimation.addUpdateListener(valueAnimator -> {
+            int newAlpha = (int) valueAnimator.getAnimatedValue();
+            int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
+
+            newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP));
+        });
+        iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                newIcon.setColorFilter(null);
+            }
+        });
+        iconUpdateAnimation.start();
+    }
+
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
index 10ea7db..09517e1 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -16,9 +16,15 @@
 
 package com.android.launcher3.widget.model;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
 
+import java.lang.annotation.Retention;
+
 /** Holder class to store the package information of an entry shown in the widgets list. */
 public abstract class WidgetsListBaseEntry {
     public final PackageItemInfo mPkgItem;
@@ -33,4 +39,22 @@
         mPkgItem = pkgItem;
         mTitleSectionName = titleSectionName;
     }
+
+    /**
+     * Returns the ranking of this entry in the
+     * {@link com.android.launcher3.widget.picker.WidgetsListAdapter}.
+     *
+     * <p>Entries with smaller value should be shown first. See
+     * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter} for more details.
+     */
+    @Rank
+    public abstract int getRank();
+
+    @Retention(SOURCE)
+    @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_CONTENT})
+    public @interface Rank {
+    }
+
+    public static final int RANK_WIDGETS_LIST_HEADER = 1;
+    public static final int RANK_WIDGETS_LIST_CONTENT = 2;
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
index 407f194..b0cb8c7 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -41,4 +41,10 @@
     public String toString() {
         return mPkgItem.packageName + ":" + mWidgets.size();
     }
+
+    @Override
+    @Rank
+    public int getRank() {
+        return RANK_WIDGETS_LIST_CONTENT;
+    }
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
new file mode 100644
index 0000000..6899647
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.Collection;
+
+/** An information holder for an app which has widgets or/and shortcuts. */
+public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
+
+    public final int widgetsCount;
+    public final int shortcutsCount;
+
+    private boolean mIsWidgetListShown = false;
+    private boolean mHasEntryUpdated = false;
+
+    public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+            Collection<WidgetItem> items) {
+        super(pkgItem, titleSectionName);
+        widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
+        shortcutsCount = Math.max(0, items.size() - widgetsCount);
+    }
+
+    /** Sets if the widgets list associated with this header is shown. */
+    public void setIsWidgetListShown(boolean isWidgetListShown) {
+        if (mIsWidgetListShown != isWidgetListShown) {
+            this.mIsWidgetListShown = isWidgetListShown;
+            mHasEntryUpdated = true;
+        } else {
+            mHasEntryUpdated = false;
+        }
+    }
+
+    /** Returns {@code true} if the widgets list associated with this header is shown. */
+    public boolean isWidgetListShown() {
+        return mIsWidgetListShown;
+    }
+
+    /** Returns {@code true} if this entry has been updated due to user interactions. */
+    public boolean hasEntryUpdated() {
+        return mHasEntryUpdated;
+    }
+
+    @Override
+    @Rank
+    public int getRank() {
+        return RANK_WIDGETS_LIST_HEADER;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
index 398d9ba..dbd1bdf 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -24,10 +24,12 @@
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
 
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 
 /**
  * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
@@ -50,7 +52,7 @@
      * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods.
      */
     public void process(ArrayList<WidgetsListBaseEntry> currentEntries,
-            ArrayList<WidgetsListBaseEntry> newEntries,
+            List<WidgetsListBaseEntry> newEntries,
             WidgetListBaseRowEntryComparator comparator) {
         if (DEBUG) {
             Log.d(TAG, "process oldEntries#=" + currentEntries.size()
@@ -78,7 +80,7 @@
         WidgetsListBaseEntry newRowEntry = newIter.next();
 
         do {
-            int diff = comparePackageName(orgRowEntry, newRowEntry, comparator);
+            int diff = compareAppNameAndType(orgRowEntry, newRowEntry, comparator);
             if (DEBUG) {
                 Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
                         diff, orgRowEntry != null ? orgRowEntry.toString() : null,
@@ -106,11 +108,13 @@
                 mListener.notifyItemInserted(index);
 
             } else {
-                // same package name but,
+                // same app name & type but,
                 // did the icon, title, etc, change?
+                // or did the header view changed due to user interactions?
                 // or did the widget size and desc, span, etc change?
                 if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
-                        || !areWidgetsEqual(orgRowEntry, newRowEntry)) {
+                        || hasHeaderUpdated(newRowEntry)
+                        || hasWidgetsListChanged(orgRowEntry, newRowEntry)) {
                     index = currentEntries.indexOf(orgRowEntry);
                     currentEntries.set(index, newRowEntry);
                     mListener.notifyItemChanged(index);
@@ -126,10 +130,13 @@
     }
 
     /**
-     * Compare package name using the same comparator as in {@link WidgetsListAdapter}.
-     * Also handle null row pointers.
+     * Compares the app name and then entry type for the given {@link WidgetsListBaseEntry}s.
+     *
+     * @Return 0 if both entries' order is the same. Negative integer if {@code newRowEntry} should
+     *         order before {@code orgRowEntry}. Positive integer if {@code orgRowEntry} should
+     *         order before {@code newRowEntry}.
      */
-    private int comparePackageName(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow,
+    private int compareAppNameAndType(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow,
             WidgetListBaseRowEntryComparator comparator) {
         if (curRow == null && newRow == null) {
             throw new IllegalStateException(
@@ -141,10 +148,18 @@
         } else if (curRow != null && newRow == null) {
             return -1; // old row needs to be deleted
         }
-        return comparator.compare(curRow, newRow);
+        int diff = comparator.compare(curRow, newRow);
+        if (diff == 0) {
+            return newRow.getRank() - curRow.getRank();
+        }
+        return diff;
     }
 
-    private boolean areWidgetsEqual(WidgetsListBaseEntry curRow,
+    /**
+     * Returns {@code true} if both {@code curRow} & {@code newRow} are
+     * {@link WidgetsListContentEntry}s with a different list of widgets.
+     */
+    private boolean hasWidgetsListChanged(WidgetsListBaseEntry curRow,
             WidgetsListBaseEntry newRow) {
         if (!(curRow instanceof WidgetsListContentEntry)
                 || !(newRow instanceof WidgetsListContentEntry)) {
@@ -152,7 +167,19 @@
         }
         WidgetsListContentEntry orgRowEntry = (WidgetsListContentEntry) curRow;
         WidgetsListContentEntry newRowEntry = (WidgetsListContentEntry) newRow;
-        return orgRowEntry.mWidgets.equals(newRowEntry.mWidgets);
+        return !orgRowEntry.mWidgets.equals(newRowEntry.mWidgets);
+    }
+
+    /**
+     * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has
+     * been changed due to user interactions.
+     */
+    private boolean hasHeaderUpdated(WidgetsListBaseEntry newRow) {
+        if (!(newRow instanceof WidgetsListHeaderEntry)) {
+            return false;
+        }
+        WidgetsListHeaderEntry newRowEntry = (WidgetsListHeaderEntry) newRow;
+        return newRowEntry.hasEntryUpdated();
     }
 
     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 9d30842..5ec7f3b 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -24,6 +24,7 @@
 import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
 
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.Adapter;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -36,32 +37,42 @@
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
- * List view adapter for the widget tray.
+ * Recycler view adapter for the widget tray.
  *
- * <p>Memory vs. Performance:
- * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling
- * happens and less memory is consumed.
+ * <p>This adapter supports view binding of subclasses of {@link WidgetsListBaseEntry}. There are 2
+ * subclasses: {@link WidgetsListHeader} & {@link WidgetsListContentEntry}.
+ * {@link WidgetsListHeader} entries are always visible in the recycler view. At most one
+ * {@link WidgetsListContentEntry} is shown in the recycler view at any time. Clicking a
+ * {@link WidgetsListHeader} will result in expanding / collapsing a corresponding
+ * {@link WidgetsListContentEntry} of the same app.
  */
-public class WidgetsListAdapter extends Adapter<ViewHolder> {
+public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderClickListener {
 
     private static final String TAG = "WidgetsListAdapter";
     private static final boolean DEBUG = false;
 
     /** Uniquely identifies widgets list view type within the app. */
     private static final int VIEW_TYPE_WIDGETS_LIST = R.layout.widgets_list_row_view;
+    private static final int VIEW_TYPE_WIDGETS_HEADER = R.layout.widgets_list_row_header;
 
     private final WidgetsDiffReporter mDiffReporter;
     private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
     private final WidgetsListRowViewHolderBinder mWidgetsListRowViewHolderBinder;
+    private final WidgetListBaseRowEntryComparator mRowComparator =
+            new WidgetListBaseRowEntryComparator();
 
-    private ArrayList<WidgetsListBaseEntry> mEntries = new ArrayList<>();
+    private List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
+    private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
+    @Nullable private String mWidgetsContentVisiblePackage = null;
 
     public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
             WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
@@ -70,6 +81,8 @@
         mWidgetsListRowViewHolderBinder = new WidgetsListRowViewHolderBinder(context,
                 layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader);
         mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListRowViewHolderBinder);
+        mViewHolderBinders.put(VIEW_TYPE_WIDGETS_HEADER,
+                new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked));
     }
 
     /**
@@ -96,26 +109,39 @@
 
     @Override
     public int getItemCount() {
-        return mEntries.size();
+        return mVisibleEntries.size();
     }
 
     /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
     public String getSectionName(int pos) {
-        return mEntries.get(pos).mTitleSectionName;
+        return mVisibleEntries.get(pos).mTitleSectionName;
     }
 
     /** Updates the widget list. */
     public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
-        ArrayList<WidgetsListBaseEntry> newEntries = new ArrayList<>(tempEntries);
-        WidgetListBaseRowEntryComparator rowComparator = new WidgetListBaseRowEntryComparator();
-        Collections.sort(newEntries, rowComparator);
-        mDiffReporter.process(mEntries, newEntries, rowComparator);
+        mAllEntries = tempEntries.stream().sorted(mRowComparator)
+                .collect(Collectors.toList());
+        updateVisibleEntries();
+    }
+
+    private void updateVisibleEntries() {
+        mAllEntries.forEach(entry -> {
+            if (entry instanceof WidgetsListHeaderEntry) {
+                ((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
+                        entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage));
+            }
+        });
+        List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
+                .filter(entry -> entry instanceof WidgetsListHeaderEntry
+                        || entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage))
+                .collect(Collectors.toList());
+        mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
     }
 
     @Override
     public void onBindViewHolder(ViewHolder holder, int pos) {
         ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
-        viewHolderBinder.bindViewHolder(holder, mEntries.get(pos));
+        viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos));
     }
 
     @Override
@@ -148,13 +174,26 @@
 
     @Override
     public int getItemViewType(int pos) {
-        WidgetsListBaseEntry entry = mEntries.get(pos);
+        WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
         if (entry instanceof WidgetsListContentEntry) {
             return VIEW_TYPE_WIDGETS_LIST;
+        } else if (entry instanceof WidgetsListHeaderEntry) {
+            return VIEW_TYPE_WIDGETS_HEADER;
         }
         throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
     }
 
+    @Override
+    public void onHeaderClicked(boolean showWidgets, String expandedPackage) {
+        if (showWidgets) {
+            mWidgetsContentVisiblePackage = expandedPackage;
+            updateVisibleEntries();
+        } else if (expandedPackage.equals(mWidgetsContentVisiblePackage)) {
+            mWidgetsContentVisiblePackage = null;
+            updateVisibleEntries();
+        }
+    }
+
     /** Comparator for sorting WidgetListRowEntry based on package title. */
     public static class WidgetListBaseRowEntryComparator implements
             Comparator<WidgetsListBaseEntry> {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
new file mode 100644
index 0000000..823fb7b
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.graphics.PlaceHolderIconDrawable;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.icons.cache.HandlerRunnable;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+/**
+ * A UI represents a header of an app shown in the full widgets tray.
+ *
+ * It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox
+ * which indicates if the widgets content view underneath this header should be shown.
+ */
+public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
+
+    private boolean mEnableIconUpdateAnimation = false;
+
+    @Nullable private HandlerRunnable mIconLoadRequest;
+    @Nullable private Drawable mIconDrawable;
+    private final int mIconSize;
+
+    private ImageView mAppIcon;
+    private TextView mTitle;
+    private TextView mSubtitle;
+
+    private CheckBox mExpandToggle;
+    private boolean mIsExpanded = false;
+
+    public WidgetsListHeader(Context context) {
+        this(context, /* attrs= */ null);
+    }
+
+    public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, /* defStyle= */ 0);
+    }
+
+    public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        ActivityContext activity = ActivityContext.lookupContext(context);
+        DeviceProfile grid = activity.getDeviceProfile();
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
+        mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
+                grid.iconSizePx);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mAppIcon = findViewById(R.id.app_icon);
+        mTitle = findViewById(R.id.app_title);
+        mSubtitle = findViewById(R.id.app_subtitle);
+        mExpandToggle = findViewById(R.id.toggle);
+    }
+
+    /**
+     * Sets a {@link OnExpansionChangeListener} to get a callback when this app widgets section
+     * expands / collapses.
+     */
+    @UiThread
+    public void setOnExpandChangeListener(
+            @Nullable OnExpansionChangeListener onExpandChangeListener) {
+        // Use the entire touch area of this view to expand / collapse an app widgets section.
+        setOnClickListener(view -> {
+            setExpanded(!mIsExpanded);
+            onExpandChangeListener.onExpansionChange(mIsExpanded);
+        });
+    }
+
+    /** Sets the expand toggle to expand / collapse. */
+    @UiThread
+    public void setExpanded(boolean isExpanded) {
+        this.mIsExpanded = isExpanded;
+        mExpandToggle.setChecked(isExpanded);
+    }
+
+    /** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
+    @UiThread
+    public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
+        applyIconAndLabel(entry);
+    }
+
+    @UiThread
+    private void applyIconAndLabel(WidgetsListHeaderEntry entry) {
+        PackageItemInfo info = entry.mPkgItem;
+        setIcon(info);
+        setTitles(entry);
+        setExpanded(entry.isWidgetListShown());
+
+        super.setTag(info);
+
+        verifyHighRes();
+    }
+
+    private void setIcon(PackageItemInfo info) {
+        FastBitmapDrawable icon = newIcon(getContext(), info);
+        applyDrawables(icon);
+        mIconDrawable = icon;
+        if (mIconDrawable != null) {
+            mIconDrawable.setVisible(
+                    /* visible= */ getWindowVisibility() == VISIBLE && isShown(),
+                    /* restart= */ false);
+        }
+    }
+
+    private void applyDrawables(Drawable icon) {
+        icon.setBounds(0, 0, mIconSize, mIconSize);
+
+        mAppIcon.setImageDrawable(icon);
+
+        // If the current icon is a placeholder color, animate its update.
+        if (mIconDrawable != null
+                && mIconDrawable instanceof PlaceHolderIconDrawable
+                && mEnableIconUpdateAnimation) {
+            ((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon);
+        }
+    }
+
+    private void setTitles(WidgetsListHeaderEntry entry) {
+        mTitle.setText(entry.mPkgItem.title);
+
+        if (entry.widgetsCount > 0) {
+            Resources resources = getContext().getResources();
+            mSubtitle.setText(resources.getQuantityString(R.plurals.widgets_tray_subtitle,
+                    entry.widgetsCount, entry.widgetsCount));
+            mSubtitle.setVisibility(VISIBLE);
+        } else {
+            mSubtitle.setVisibility(GONE);
+        }
+    }
+
+    @Override
+    public void reapplyItemInfo(ItemInfoWithIcon info) {
+        if (getTag() == info) {
+            mIconLoadRequest = null;
+            mEnableIconUpdateAnimation = true;
+
+            // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
+            info.bitmap.icon.prepareToDraw();
+
+            setIcon((PackageItemInfo) info);
+
+            mEnableIconUpdateAnimation = false;
+        }
+    }
+
+    /** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
+    public void verifyHighRes() {
+        if (mIconLoadRequest != null) {
+            mIconLoadRequest.cancel();
+            mIconLoadRequest = null;
+        }
+        if (getTag() instanceof ItemInfoWithIcon) {
+            ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
+            if (info.usingLowResIcon()) {
+                mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
+                        .updateIconInBackground(this, info);
+            }
+        }
+    }
+
+    /** A listener for the widget section expansion / collapse events. */
+    public interface OnExpansionChangeListener {
+        /** Notifies that the widget section is expanded or collapsed. */
+        void onExpansionChange(boolean isExpanded);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java
new file mode 100644
index 0000000..d4e1b1c
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app
+ * name, label and a button for showing / hiding widgets.
+ */
+public final class WidgetsListHeaderHolder extends ViewHolder {
+    final WidgetsListHeader mWidgetsListHeader;
+
+    public WidgetsListHeaderHolder(WidgetsListHeader view) {
+        super(view);
+
+        mWidgetsListHeader = view;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
new file mode 100644
index 0000000..ed53e6f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+/**
+ * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
+ */
+public final class WidgetsListHeaderViewHolderBinder implements
+        ViewHolderBinder<WidgetsListHeaderEntry, WidgetsListHeaderHolder> {
+    private final LayoutInflater mLayoutInflater;
+    private final OnHeaderClickListener mOnHeaderClickListener;
+
+    public WidgetsListHeaderViewHolderBinder(LayoutInflater layoutInflater,
+            OnHeaderClickListener onHeaderClickListener) {
+        mLayoutInflater = layoutInflater;
+        mOnHeaderClickListener = onHeaderClickListener;
+    }
+
+    @Override
+    public WidgetsListHeaderHolder newViewHolder(ViewGroup parent) {
+        WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_header, parent, false);
+
+        return new WidgetsListHeaderHolder(header);
+    }
+
+    @Override
+    public void bindViewHolder(WidgetsListHeaderHolder viewHolder, WidgetsListHeaderEntry data) {
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        widgetsListHeader.applyFromItemInfoWithIcon(data);
+        widgetsListHeader.setExpanded(data.isWidgetListShown());
+        widgetsListHeader.setOnExpandChangeListener(isExpanded ->
+                mOnHeaderClickListener.onHeaderClicked(isExpanded, data.mPkgItem.packageName));
+    }
+
+    /** A listener to be invoked when {@link WidgetsListHeader} is clicked. */
+    public interface OnHeaderClickListener {
+        /** Calls when {@link WidgetsListHeader} is clicked to show / hide widgets for a package. */
+        void onHeaderClicked(boolean showWidgets, String packageName);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java
index 22a8d00..cec6b80 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java
@@ -76,7 +76,7 @@
         }
 
         ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
-                R.layout.widgets_list_row_view, parent, false);
+                R.layout.widgets_scroll_container, parent, false);
 
         // if the end padding is 0, then container view (horizontal scroll view) doesn't respect
         // the end of the linear layout width + the start padding and doesn't allow scrolling.
@@ -122,9 +122,6 @@
             }
         }
 
-        // Bind the views in the application info section.
-        holder.title.applyFromItemInfoWithIcon(entry.mPkgItem);
-
         // Bind the view in the widget horizontal tray region.
         for (int i = 0; i < infoList.size(); i++) {
             WidgetCell widget = (WidgetCell) row.getChildAt(2 * i);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
index 9be079e..ae94584 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
@@ -19,20 +19,16 @@
 
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
-import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
 
-/** A {@link ViewHolder} for a row in the full widget picker. */
+/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
 public final class WidgetsRowViewHolder extends ViewHolder {
 
     public final ViewGroup cellContainer;
-    public final BubbleTextView title;
 
     public WidgetsRowViewHolder(ViewGroup v) {
         super(v);
 
         cellContainer = v.findViewById(R.id.widgets_cell_list);
-        title = v.findViewById(R.id.section);
-        title.setAccessibilityDelegate(null);
     }
 }