Merge "Revert "Ignore broken launcher tests"" into sc-dev
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 89b3831..a202095 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -25,7 +25,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -43,17 +43,17 @@
     public static final boolean GO_DISABLE_WIDGETS = true;
     public static final boolean GO_DISABLE_NOTIFICATION_DOTS = true;
 
-    private static final ArrayList<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
+    private static final ArrayList<WidgetsListBaseEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
 
     /**
-     * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
-     * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
-     * is not sorted. This list is sorted at the UI when using
-     * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+     * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row are
+     * sorted (based on label and user), but the overall list of {@link WidgetsListBaseEntry}s is
+     * not sorted. This list is sorted at the UI when using
+     * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter}
      *
-     * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
      */
-    public synchronized ArrayList<WidgetListRowEntry> getWidgetsList(Context context) {
+    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsList(Context context) {
         return EMPTY_WIDGET_LIST;
     }
 
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 7431551..97f4a21 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -39,6 +39,7 @@
     <uses-permission android:name="android.permission.SET_ORIENTATION"/>
     <uses-permission android:name="android.permission.READ_FRAME_BUFFER"/>
     <uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY"/>
+    <uses-permission android:name="android.permission.MONITOR_INPUT"/>
 
     <uses-permission android:name="${packageName}.permission.HOTSEAT_EDU" />
     <uses-permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY" />
diff --git a/quickstep/res/layout/task_menu.xml b/quickstep/res/layout/task_menu.xml
index 098b34f..744a305 100644
--- a/quickstep/res/layout/task_menu.xml
+++ b/quickstep/res/layout/task_menu.xml
@@ -24,15 +24,6 @@
     android:orientation="vertical"
     android:visibility="invisible">
 
-    <com.android.quickstep.views.IconView
-      android:id="@+id/task_icon"
-      android:layout_width="@dimen/task_thumbnail_icon_size"
-      android:layout_height="@dimen/task_thumbnail_icon_size"
-      android:layout_gravity="top|center_horizontal"
-      android:layout_marginBottom="@dimen/deep_shortcut_drawable_padding"
-      android:focusable="false"
-      android:importantForAccessibility="no" />
-
     <TextView
         android:id="@+id/task_name"
         android:layout_width="match_parent"
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 0f40775..749b4b2 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -19,6 +19,7 @@
     <dimen name="task_thumbnail_top_margin">24dp</dimen>
     <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
+    <dimen name="task_icon_top_margin">-16dp</dimen>
     <!-- For screens without rounded corners -->
     <dimen name="task_corner_radius_small">2dp</dimen>
 
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 36b51cd..feeee50 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1071,9 +1071,7 @@
                     && runningTaskTarget != null
                     && runningTaskTarget.pictureInPictureParams != null
                     && TaskInfoCompat.isAutoEnterPipEnabled(
-                            runningTaskTarget.pictureInPictureParams)
-                    && TaskInfoCompat.getPipSourceRectHint(
-                            runningTaskTarget.pictureInPictureParams) != null;
+                            runningTaskTarget.pictureInPictureParams);
             if (mIsSwipingPipToHome) {
                 mSwipePipToHomeAnimator = getSwipePipToHomeAnimator(
                         homeAnimFactory, runningTaskTarget, start);
@@ -1176,8 +1174,10 @@
             swipePipToHomeAnimator.setFromRotation(mTaskViewSimulator, windowRotation);
         }
         swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mHasAnimationEnded;
             @Override
             public void onAnimationStart(Animator animation) {
+                if (mHasAnimationEnded) return;
                 // Ensure Launcher ends in NORMAL state, we intentionally skip other callbacks
                 // since they are not relevant in this swipe-pip-to-home case.
                 homeAnimFactory.createActivityAnimationToHome().dispatchOnStart();
@@ -1185,6 +1185,8 @@
 
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (mHasAnimationEnded) return;
+                mHasAnimationEnded = true;
                 if (mRecentsAnimationController == null) {
                     // If the recents animation is interrupted, we still end the running
                     // animation (not canceled) so this is still called. In that case, we can
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 8ebea33..e243715 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,7 +24,6 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 
@@ -283,9 +282,7 @@
             return;
         }
 
-        Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
-                mDeviceState.getDisplayId());
-        mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
+        mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId());
         mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                 mMainChoreographer, this::onInputEvent);
 
diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 5bae3c7..932ff27 100644
--- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -139,7 +139,7 @@
 
         addDepthAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
 
-        mAnimators.play(launcher.getDragLayer().getScrim().createSysuiMultiplierAnim(0f, 1f)
+        mAnimators.play(launcher.getDragLayer().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f)
                 .setDuration(ALPHA_DURATION_MS));
         mAnimators.addListener(new AnimatorListenerAdapter() {
             @Override
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 378f25b..0ce5072 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -31,6 +31,7 @@
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
@@ -60,7 +61,7 @@
     /** for calculating the transform in {@link #onAnimationUpdate(ValueAnimator)} */
     private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
     private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
-    private final Rect mSourceHintRectInsets = new Rect();
+    private final Rect mSourceHintRectInsets;
     private final Rect mSourceInsets = new Rect();
 
     /** for rotation via {@link #setFromRotation(TaskViewSimulator, int)} */
@@ -89,7 +90,7 @@
     public SwipePipToHomeAnimator(int taskId,
             @NonNull ComponentName componentName,
             @NonNull SurfaceControl leash,
-            @NonNull Rect sourceRectHint,
+            @Nullable Rect sourceRectHint,
             @NonNull Rect appBounds,
             @NonNull Rect startBounds,
             @NonNull Rect destinationBounds,
@@ -104,10 +105,14 @@
         mDestinationBoundsAnimation.set(mDestinationBounds);
         mSurfaceTransactionHelper = new PipSurfaceTransactionHelper();
 
-        mSourceHintRectInsets.set(sourceRectHint.left - appBounds.left,
-                sourceRectHint.top - appBounds.top,
-                appBounds.right - sourceRectHint.right,
-                appBounds.bottom - sourceRectHint.bottom);
+        if (sourceRectHint == null) {
+            mSourceHintRectInsets = null;
+        } else {
+            mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left,
+                    sourceRectHint.top - appBounds.top,
+                    appBounds.right - sourceRectHint.right,
+                    appBounds.bottom - sourceRectHint.bottom);
+        }
 
         addListener(new AnimationSuccessListener() {
             @Override
@@ -168,34 +173,44 @@
         final float fraction = animator.getAnimatedFraction();
         final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds,
                 mDestinationBoundsAnimation);
-        final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
-                mSourceHintRectInsets);
         final SurfaceControl.Transaction tx =
                 PipSurfaceTransactionHelper.newSurfaceControlTransaction();
-        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
-            final float degree, positionX, positionY;
-            if (mFromRotation == Surface.ROTATION_90) {
-                degree = -90 * fraction;
-                positionX = fraction * (mDestinationBoundsTransformed.left - mAppBounds.left)
-                        + mAppBounds.left;
-                positionY = fraction * (mDestinationBoundsTransformed.bottom - mAppBounds.top)
-                        + mAppBounds.top;
-            } else {
-                degree = 90 * fraction;
-                positionX = fraction * (mDestinationBoundsTransformed.right - mAppBounds.left)
-                        + mAppBounds.left;
-                positionY = fraction * (mDestinationBoundsTransformed.top - mAppBounds.top)
-                        + mAppBounds.top;
-            }
-            mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
-                    degree, positionX, positionY);
+        if (mSourceHintRectInsets == null) {
+            // no source rect hint been set, directly scale the window down
+            onAnimationScale(fraction, tx, bounds);
         } else {
-            mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets);
+            // scale and crop according to the source rect hint
+            onAnimationScaleAndCrop(fraction, tx, bounds);
         }
         mSurfaceTransactionHelper.resetCornerRadius(tx, mLeash);
         tx.apply();
     }
 
+    /** scale the window directly with no source rect hint being set */
+    private void onAnimationScale(float fraction, SurfaceControl.Transaction tx, Rect bounds) {
+        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+            final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
+            mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds,
+                    rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
+        } else {
+            mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds);
+        }
+    }
+
+    /** scale and crop the window with source rect hint */
+    private void onAnimationScaleAndCrop(float fraction, SurfaceControl.Transaction tx,
+            Rect bounds) {
+        final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
+                mSourceHintRectInsets);
+        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+            final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
+            mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
+                    rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
+        } else {
+            mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets);
+        }
+    }
+
     public int getTaskId() {
         return mTaskId;
     }
@@ -217,4 +232,34 @@
         tx.apply();
         mHasAnimationEnded = true;
     }
+
+    private RotatedPosition getRotatedPosition(float fraction) {
+        final float degree, positionX, positionY;
+        if (mFromRotation == Surface.ROTATION_90) {
+            degree = -90 * fraction;
+            positionX = fraction * (mDestinationBoundsTransformed.left - mAppBounds.left)
+                    + mAppBounds.left;
+            positionY = fraction * (mDestinationBoundsTransformed.bottom - mAppBounds.top)
+                    + mAppBounds.top;
+        } else {
+            degree = 90 * fraction;
+            positionX = fraction * (mDestinationBoundsTransformed.right - mAppBounds.left)
+                    + mAppBounds.left;
+            positionY = fraction * (mDestinationBoundsTransformed.top - mAppBounds.top)
+                    + mAppBounds.top;
+        }
+        return new RotatedPosition(degree, positionX, positionY);
+    }
+
+    private static class RotatedPosition {
+        private final float degree;
+        private final float positionX;
+        private final float positionY;
+
+        private RotatedPosition(float degree, float positionX, float positionY) {
+            this.degree = degree;
+            this.positionX = positionX;
+            this.positionY = positionY;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 4aff7e3..7e558bb 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -35,7 +35,6 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
@@ -46,7 +45,6 @@
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.views.IconView.OnScaleUpdateListener;
 
 /**
  * Contains options for a recent task when long-pressing its icon.
@@ -55,42 +53,15 @@
 
     private static final Rect sTempRect = new Rect();
 
-    private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() {
-        @Override
-        public void onScaleUpdate(float scale) {
-            final Drawable drawable = mTaskIcon.getDrawable();
-            if (drawable instanceof FastBitmapDrawable) {
-                if (scale != ((FastBitmapDrawable) drawable).getScale()) {
-                    mMenuIconDrawable.setScale(scale);
-                }
-            }
-        }
-    };
-
-    private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() {
-        @Override
-        public void onScaleUpdate(float scale) {
-            final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable();
-            if (taskViewDrawable instanceof FastBitmapDrawable) {
-                final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale();
-                if (currentScale != scale) {
-                    ((FastBitmapDrawable) taskViewDrawable).setScale(scale);
-                }
-            }
-        }
-    };
-
     private static final int REVEAL_OPEN_DURATION = 150;
     private static final int REVEAL_CLOSE_DURATION = 100;
 
     private final float mThumbnailTopMargin;
     private BaseDraggingActivity mActivity;
     private TextView mTaskName;
-    private IconView mTaskIcon;
     private AnimatorSet mOpenCloseAnimator;
     private TaskView mTaskView;
     private LinearLayout mOptionLayout;
-    private FastBitmapDrawable mMenuIconDrawable;
 
     public TaskMenuView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -107,7 +78,6 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mTaskName = findViewById(R.id.task_name);
-        mTaskIcon = findViewById(R.id.task_icon);
         mOptionLayout = findViewById(R.id.menu_option_layout);
     }
 
@@ -134,15 +104,6 @@
     }
 
     @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-
-        // Remove all scale listeners when menu is removed
-        mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener);
-        mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener);
-    }
-
-    @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_TASK_MENU) != 0;
     }
@@ -204,22 +165,9 @@
 
     private void addMenuOptions(TaskView taskView) {
         Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
-        mTaskIcon.setDrawable(icon);
-        mTaskIcon.setOnClickListener(v -> close(true));
         mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
         mTaskName.setOnClickListener(v -> close(true));
 
-        // Set the icons to match scale by listening to each other's changes
-        mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null;
-        taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener);
-        mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener);
-
-        // Move the icon and text up half an icon size to lay over the TaskView
-        LinearLayout.LayoutParams params =
-                (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams();
-        params.topMargin = (int) -mThumbnailTopMargin;
-        mTaskIcon.setLayoutParams(params);
-
         TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index f420de6..8a8b021 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -612,6 +612,9 @@
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+        int taskIconMargin = (int) getResources().getDimension(R.dimen.task_icon_top_margin);
+        int taskIconHeight = (int) getResources().getDimension(R.dimen.task_thumbnail_icon_size);
+        int iconTopMargin = taskIconMargin - taskIconHeight + thumbnailPadding;
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         switch (orientationHandler.getRotation()) {
             case ROTATION_90:
@@ -623,7 +626,8 @@
             case ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
                 iconParams.bottomMargin = -thumbnailPadding;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                iconParams.leftMargin = iconParams.rightMargin = 0;
+                iconParams.topMargin = iconTopMargin;
                 break;
             case ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
@@ -634,7 +638,8 @@
             case Surface.ROTATION_0:
             default:
                 iconParams.gravity = TOP | CENTER_HORIZONTAL;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                iconParams.leftMargin = iconParams.rightMargin = 0;
+                iconParams.topMargin = iconTopMargin;
                 break;
         }
         mIconView.setLayoutParams(iconParams);
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index f507a88..6e7cf0f 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.widget.WidgetsFullSheet
+<com.android.launcher3.widget.picker.WidgetsFullSheet
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -27,7 +27,7 @@
         android:background="?android:attr/colorPrimary"
         android:elevation="4dp">
 
-        <com.android.launcher3.widget.WidgetsRecyclerView
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
             android:id="@+id/widgets_list_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
@@ -49,4 +49,4 @@
             android:layout_alignParentTop="true"
             android:layout_marginEnd="@dimen/fastscroll_end_margin" />
     </com.android.launcher3.views.TopRoundedCornerView>
-</com.android.launcher3.widget.WidgetsFullSheet>
\ No newline at end of file
+</com.android.launcher3.widget.picker.WidgetsFullSheet>
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java
new file mode 100644
index 0000000..dbf4b3e
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java
@@ -0,0 +1,41 @@
+/*
+ * 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.testing;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+
+/** An empty activity for {@link android.app.Fragment}s, {@link android.view.View}s testing. */
+public class TestActivity extends BaseActivity implements ActivityContext {
+
+    private DeviceProfile mDeviceProfile;
+
+    @Override
+    public BaseDragLayer getDragLayer() {
+        return null;
+    }
+
+    @Override
+    public DeviceProfile getDeviceProfile() {
+        return mDeviceProfile;
+    }
+
+    public void setDeviceProfile(DeviceProfile deviceProfile) {
+        mDeviceProfile = deviceProfile;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
index 957ae77..34cb2ad 100644
--- a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
+++ b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
@@ -38,7 +38,7 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherLayoutBuilder.FolderBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
deleted file mode 100644
index 5ab3106..0000000
--- a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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.widget;
-
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.robolectric.Shadows.shadowOf;
-
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.view.LayoutInflater;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowPackageManager;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-@RunWith(RobolectricTestRunner.class)
-public class WidgetsListAdapterTest {
-
-    @Mock private LayoutInflater mMockLayoutInflater;
-    @Mock private WidgetPreviewLoader mMockWidgetCache;
-    @Mock private RecyclerView.AdapterDataObserver mListener;
-    @Mock private IconCache mIconCache;
-
-    private WidgetsListAdapter mAdapter;
-    private InvariantDeviceProfile mTestProfile;
-    private Context mContext;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
-        mTestProfile = new InvariantDeviceProfile();
-        mTestProfile.numRows = 5;
-        mTestProfile.numColumns = 5;
-        mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
-                mIconCache, null, null);
-        mAdapter.registerAdapterDataObserver(mListener);
-    }
-
-    @Test
-    public void test_notifyDataSetChanged() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(1));
-        verify(mListener, times(1)).onChanged();
-    }
-
-    @Test
-    public void test_notifyItemInserted() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(1));
-        mAdapter.setWidgets(generateSampleMap(2));
-        verify(mListener, times(1)).onChanged();
-        verify(mListener, times(1)).onItemRangeInserted(eq(1), eq(1));
-    }
-
-    @Test
-    public void test_notifyItemRemoved() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(2));
-        mAdapter.setWidgets(generateSampleMap(1));
-        verify(mListener, times(1)).onChanged();
-        verify(mListener, times(1)).onItemRangeRemoved(eq(1), eq(1));
-    }
-
-    @Test
-    public void testNotifyItemChanged_PackageIconDiff() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(1));
-        mAdapter.setWidgets(generateSampleMap(1));
-        verify(mListener, times(1)).onChanged();
-        verify(mListener, times(1)).onItemRangeChanged(eq(0), eq(1), isNull());
-    }
-
-    @Test
-    public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
-        // TODO: same package name but item number changed
-    }
-
-    @Test
-    public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception {
-        // TODO: insert and remove combined.          curMap
-        // newMap [A, C, D]                           [A, B, E]
-        // B - C < 0, removed B from index 1          [A, E]
-        // E - C > 0, C inserted to index 1           [A, C, E]
-        // E - D > 0, D inserted to index 2           [A, C, D, E]
-        // E - null = -1, E deleted from index 3      [A, C, D]
-    }
-
-    /**
-     * Helper method to generate the sample widget model map that can be used for the tests
-     * @param num the number of WidgetItem the map should contain
-     */
-    private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
-        ArrayList<WidgetListRowEntry> result = new ArrayList<>();
-        if (num <= 0) return result;
-        ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
-
-        for (int i = 0; i < num; i++) {
-            ComponentName cn = new ComponentName("com.placeholder.apk" + i, "PlaceholderWidet");
-
-            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
-            widgetInfo.provider = cn;
-            ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn));
-
-            WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
-                    .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
-
-            PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
-            pInfo.title = pInfo.packageName;
-            pInfo.user = wi.user;
-            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
-
-            result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi))));
-        }
-
-        return result;
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
new file mode 100644
index 0000000..9bea2fb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.widget.picker;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListAdapterTest {
+
+    private static final String TEST_PACKAGE_1 = "com.google.test.1";
+    private static final String TEST_PACKAGE_2 = "com.google.test.2";
+
+    @Mock private LayoutInflater mMockLayoutInflater;
+    @Mock private WidgetPreviewLoader mMockWidgetCache;
+    @Mock private RecyclerView.AdapterDataObserver mListener;
+    @Mock private IconCache mIconCache;
+
+    private WidgetsListAdapter mAdapter;
+    private InvariantDeviceProfile mTestProfile;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+        mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
+                mIconCache, null, null);
+        mAdapter.registerAdapterDataObserver(mListener);
+
+        doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+                        .getComponent().getPackageName())
+                .when(mIconCache).getTitleNoCache(any());
+    }
+
+    @Test
+    public void setWidgets_shouldNotifyDataSetChanged() {
+        mAdapter.setWidgets(generateSampleMap(1));
+
+        verify(mListener).onChanged();
+    }
+
+    @Test
+    public void setWidgets_withItemInserted_shouldNotifyItemInserted() {
+        mAdapter.setWidgets(generateSampleMap(1));
+        mAdapter.setWidgets(generateSampleMap(2));
+
+        verify(mListener).onItemRangeInserted(eq(1), eq(1));
+    }
+
+    @Test
+    public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() {
+        mAdapter.setWidgets(generateSampleMap(2));
+        mAdapter.setWidgets(generateSampleMap(1));
+
+        verify(mListener).onItemRangeRemoved(eq(1), eq(1));
+    }
+
+    @Test
+    public void setWidgets_appIconChanged_shouldNotifyItemChanged() {
+        mAdapter.setWidgets(generateSampleMap(1));
+        mAdapter.setWidgets(generateSampleMap(1));
+
+        verify(mListener).onItemRangeChanged(eq(0), eq(1), isNull());
+    }
+
+    @Test
+    public void setWidgets_sameApp_moreWidgets_shouldNotifyItemChangedWithWidgetItemInfoDiff() {
+        // GIVEN the adapter was first populated with test package 1 & test package 2.
+        WidgetsListBaseEntry testPackage1With2WidgetsListEntry =
+                generateSampleAppWithWidgets(TEST_PACKAGE_1, /* numOfWidgets= */ 2);
+        WidgetsListBaseEntry testPackage2With2WidgetsListEntry =
+                generateSampleAppWithWidgets(TEST_PACKAGE_2, /* numOfWidgets= */ 2);
+        mAdapter.setWidgets(
+                List.of(testPackage1With2WidgetsListEntry, testPackage2With2WidgetsListEntry));
+
+        // WHEN the adapter is updated with the same list of apps but test package 2 has 3 widgets
+        // now.
+        WidgetsListBaseEntry testPackage1With3WidgetsListEntry =
+                generateSampleAppWithWidgets(TEST_PACKAGE_2, /* numOfWidgets= */ 2);
+        mAdapter.setWidgets(
+                List.of(testPackage1With2WidgetsListEntry, testPackage1With3WidgetsListEntry));
+
+        // THEN the onItemRangeChanged is invoked.
+        verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull());
+    }
+
+    @Test
+    public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() {
+        List<WidgetsListBaseEntry> allAppsWithWidgets = generateSampleMap(5);
+        // GIVEN the current widgets list consist of [A, B, E].
+        List<WidgetsListBaseEntry> currentList = List.of(
+                allAppsWithWidgets.get(0), allAppsWithWidgets.get(1), allAppsWithWidgets.get(4));
+        mAdapter.setWidgets(currentList);
+
+        // WHEN the widgets list is updated to [A, C, D].
+        List<WidgetsListBaseEntry> newList = List.of(
+                allAppsWithWidgets.get(0), allAppsWithWidgets.get(2), allAppsWithWidgets.get(3));
+        mAdapter.setWidgets(newList);
+
+        // Computation logic                           | [Intermediate list during computation]
+        // THEN B <> C < 0, removed B from index 1     | [A, E]
+        verify(mListener).onItemRangeRemoved(/* positionStart= */ 1, /* itemCount= */ 1);
+        // THEN E <> C > 0, C inserted to index 1      | [A, C, E]
+        verify(mListener).onItemRangeInserted(/* positionStart= */ 1, /* itemCount= */ 1);
+        // THEN E <> D > 0, D inserted to index 2      | [A, C, D, E]
+        verify(mListener).onItemRangeInserted(/* positionStart= */ 2, /* itemCount= */ 1);
+        // THEN E <> null = -1, E deleted from index 3 | [A, C, D]
+        verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
+    }
+
+    /**
+     * Helper method to generate the sample widget model map that can be used for the tests
+     * @param num the number of WidgetItem the map should contain
+     */
+    private ArrayList<WidgetsListBaseEntry> generateSampleMap(int num) {
+        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
+        if (num <= 0) return result;
+
+        for (int i = 0; i < num; i++) {
+            String packageName = "com.placeholder.apk" + i;
+
+            List<WidgetItem> widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1);
+
+            PackageItemInfo pInfo = new PackageItemInfo(packageName);
+            pInfo.title = pInfo.packageName;
+            pInfo.user = widgetItems.get(0).user;
+            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+            result.add(new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems));
+        }
+
+        return result;
+    }
+
+    private WidgetsListBaseEntry generateSampleAppWithWidgets(String packageName,
+            int numOfWidgets) {
+        PackageItemInfo appInfo = new PackageItemInfo(packageName);
+        appInfo.title = appInfo.packageName;
+        appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+        return new WidgetsListContentEntry(appInfo,
+                /* titleSectionName= */ "",
+                generateWidgetItems(packageName, numOfWidgets));
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            widgetItems.add(new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache));
+        }
+        return widgetItems;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinderTest.java
new file mode 100644
index 0000000..4e9e227
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinderTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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 android.os.Looper.getMainLooper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListRowViewHolderBinderTest {
+    private static final String TEST_PACKAGE = "com.google.test";
+    private static final String APP_NAME = "Test app";
+
+    private Context mContext;
+    private WidgetsListRowViewHolderBinder mViewHolderBinder;
+    private InvariantDeviceProfile mTestProfile;
+    // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+    // testing.
+    private ActivityController<TestActivity> mActivityController;
+    private TestActivity mTestActivity;
+
+    @Mock
+    private OnLongClickListener mOnLongClickListener;
+    @Mock
+    private OnClickListener mOnIconClickListener;
+    @Mock
+    private IconCache mIconCache;
+    @Mock
+    private WidgetPreviewLoader mWidgetPreviewLoader;
+    @Mock
+    private DeviceProfile mDeviceProfile;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        mActivityController = Robolectric.buildActivity(TestActivity.class);
+        mTestActivity = mActivityController.setup().get();
+        mTestActivity.setDeviceProfile(mDeviceProfile);
+
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return componentWithLabel.getComponent().getShortClassName();
+        }).when(mIconCache).getTitleNoCache(any());
+
+        mViewHolderBinder = new WidgetsListRowViewHolderBinder(
+                mContext,
+                LayoutInflater.from(mTestActivity),
+                mOnIconClickListener,
+                mOnLongClickListener,
+                mWidgetPreviewLoader);
+    }
+
+    @After
+    public void tearDown() {
+        mActivityController.destroy();
+    }
+
+    @Test
+    public void bindViewHolder_appWith3Widgets_shouldMatchAppTitle() {
+        WidgetsRowViewHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListContentEntry entry = generateSampleAppWithWidgets(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+
+        assertThat(viewHolder.title.getText()).isEqualTo(APP_NAME);
+    }
+
+    @Test
+    public void bindViewHolder_appWith3Widgets_shouldHave3Widgets() {
+        WidgetsRowViewHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListContentEntry entry = generateSampleAppWithWidgets(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+        shadowOf(getMainLooper()).idle();
+
+        // THEN the cell container has 5 children: 3 widgets + 2 separators
+        // Index:        0       1        2       3        4
+        // View:  .SampleWidget0 | .SampleWidget1 | .SampleWidget2
+        assertThat(viewHolder.cellContainer.getChildCount()).isEqualTo(5);
+        // Widget 0 label is .SampleWidget0.
+        assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(0), ".SampleWidget0");
+        // Widget 1 label is .SampleWidget1.
+        assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(2), ".SampleWidget1");
+        // Widget 2 label is .SampleWidget2.
+        assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(4), ".SampleWidget2");
+    }
+
+    private WidgetsListContentEntry generateSampleAppWithWidgets(String appName, String packageName,
+            int numOfWidgets) {
+        PackageItemInfo appInfo = new PackageItemInfo(packageName);
+        appInfo.title = appName;
+        appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+        return new WidgetsListContentEntry(appInfo,
+                /* titleSectionName= */ "",
+                generateWidgetItems(packageName, numOfWidgets));
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            widgetItems.add(new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache));
+        }
+        return widgetItems;
+    }
+
+    private void assertWidgetCellWithLabel(View view, String label) {
+        assertThat(view).isInstanceOf(WidgetCell.class);
+        TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name);
+        assertThat(widgetLabel.getText()).isEqualTo(label);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
new file mode 100644
index 0000000..a11d0c9
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListContentEntryTest {
+    private static final String PACKAGE_NAME = "com.google.test";
+    private static final PackageItemInfo PACKAGE_ITEM_INFO = new PackageItemInfo(PACKAGE_NAME);
+    private static final ComponentName WIDGET_1 = ComponentName.createRelative(PACKAGE_NAME,
+            ".widget1");
+    private static final ComponentName WIDGET_2 = ComponentName.createRelative(PACKAGE_NAME,
+            ".widget2");
+    private static final ComponentName WIDGET_3 = ComponentName.createRelative(PACKAGE_NAME,
+            ".widget3");
+    private static final Map<ComponentName, String> WIDGETS_TO_LABELS = new HashMap();
+
+    static {
+        WIDGETS_TO_LABELS.put(WIDGET_1, "Cat");
+        WIDGETS_TO_LABELS.put(WIDGET_2, "Dog");
+        WIDGETS_TO_LABELS.put(WIDGET_3, "Bird");
+    }
+
+    @Mock private IconCache mIconCache;
+
+    private Context mContext;
+    private InvariantDeviceProfile mTestProfile;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return WIDGETS_TO_LABELS.get(componentWithLabel.getComponent());
+        }).when(mIconCache).getTitleNoCache(any());
+    }
+
+    @Test
+    public void unsortedWidgets_diffLabels_shouldSortWidgetItems() {
+        // GIVEN a list of widgets in unsorted order.
+        // Cat 2x3
+        WidgetItem widgetItem1 = createWidgetItem(WIDGET_1, /* spanX= */ 2, /* spanY= */ 3);
+        // Dog 2x3
+        WidgetItem widgetItem2 = createWidgetItem(WIDGET_2, /* spanX= */ 2, /* spanY= */ 3);
+        // Bird 2x3
+        WidgetItem widgetItem3 = createWidgetItem(WIDGET_3, /* spanX= */ 2, /* spanY= */ 3);
+
+        // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+        WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(PACKAGE_ITEM_INFO,
+                /* titleSectionName= */ "T",
+                List.of(widgetItem1, widgetItem2, widgetItem3));
+
+        // THEN the widgets list is sorted by their labels alphabetically: [Bird, Cat, Dog].
+        assertThat(widgetsListRowEntry.mWidgets)
+                .containsExactly(widgetItem3, widgetItem1, widgetItem2)
+                .inOrder();
+        assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+        assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(PACKAGE_ITEM_INFO);
+    }
+
+    @Test
+    public void unsortedWidgets_sameLabels_differentSize_shouldSortWidgetItems() {
+        // GIVEN a list of widgets in unsorted order.
+        // Cat 3x3
+        WidgetItem widgetItem1 = createWidgetItem(WIDGET_1, /* spanX= */ 3, /* spanY= */ 3);
+        // Cat 1x2
+        WidgetItem widgetItem2 = createWidgetItem(WIDGET_1, /* spanX= */ 1, /* spanY= */ 2);
+        // Cat 2x2
+        WidgetItem widgetItem3 = createWidgetItem(WIDGET_1, /* spanX= */ 2, /* spanY= */ 2);
+
+        // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+        WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(PACKAGE_ITEM_INFO,
+                /* titleSectionName= */ "T",
+                List.of(widgetItem1, widgetItem2, widgetItem3));
+
+        // THEN the widgets list is sorted by their gird sizes in an ascending order:
+        // [1x2, 2x2, 3x3].
+        assertThat(widgetsListRowEntry.mWidgets)
+                .containsExactly(widgetItem2, widgetItem3, widgetItem1)
+                .inOrder();
+        assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+        assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(PACKAGE_ITEM_INFO);
+    }
+
+    @Test
+    public void unsortedWidgets_hodgepodge_shouldSortWidgetItems() {
+        // GIVEN a list of widgets in unsorted order.
+        // Cat 3x3
+        WidgetItem widgetItem1 = createWidgetItem(WIDGET_1, /* spanX= */ 3, /* spanY= */ 3);
+        // Cat 1x2
+        WidgetItem widgetItem2 = createWidgetItem(WIDGET_1, /* spanX= */ 1, /* spanY= */ 2);
+        // Dog 2x2
+        WidgetItem widgetItem3 = createWidgetItem(WIDGET_2, /* spanX= */ 2, /* spanY= */ 2);
+        // Bird 2x2
+        WidgetItem widgetItem4 = createWidgetItem(WIDGET_3, /* spanX= */ 2, /* spanY= */ 2);
+
+        // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+        WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(PACKAGE_ITEM_INFO,
+                /* titleSectionName= */ "T",
+                List.of(widgetItem1, widgetItem2, widgetItem3, widgetItem4));
+
+        // THEN the widgets list is first sorted by labels alphabetically. Then, for widgets with
+        // same labels, they are sorted by their gird sizes in an ascending order:
+        // [Bird 2x2, Cat 1x2, Cat 3x3, Dog 2x2]
+        assertThat(widgetsListRowEntry.mWidgets)
+                .containsExactly(widgetItem4, widgetItem2, widgetItem1, widgetItem3)
+                .inOrder();
+        assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+        assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(PACKAGE_ITEM_INFO);
+    }
+
+    private WidgetItem createWidgetItem(ComponentName componentName, int spanX, int spanY) {
+        String label = WIDGETS_TO_LABELS.get(componentName);
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+        widgetInfo.provider = componentName;
+        ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                packageManager.addReceiverIfNotPresent(componentName));
+
+        LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+                LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo);
+        launcherAppWidgetProviderInfo.spanX = spanX;
+        launcherAppWidgetProviderInfo.spanY = spanY;
+        launcherAppWidgetProviderInfo.label = label;
+
+        return new WidgetItem(launcherAppWidgetProviderInfo, mTestProfile, mIconCache);
+    }
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 344ae0a..2df9cbe 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -181,10 +181,10 @@
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 import com.android.launcher3.widget.WidgetHostViewLoader;
-import com.android.launcher3.widget.WidgetListRowEntry;
 import com.android.launcher3.widget.WidgetManagerHelper;
-import com.android.launcher3.widget.WidgetsFullSheet;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.systemui.plugins.OverlayPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.shared.LauncherExterns;
@@ -2552,7 +2552,7 @@
     }
 
     @Override
-    public void bindAllWidgets(final ArrayList<WidgetListRowEntry> allWidgets) {
+    public void bindAllWidgets(final List<WidgetsListBaseEntry> allWidgets) {
         mPopupDataProvider.setAllWidgets(allWidgets);
     }
 
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 51504ce..76c4518 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -4,12 +4,14 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.ViewDebug;
 import android.view.WindowInsets;
 
+import com.android.launcher3.graphics.SysUiScrim;
 import com.android.launcher3.statemanager.StatefulActivity;
 
 import java.util.Collections;
@@ -31,6 +33,8 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mForceHideBackArrow;
 
+    private SysUiScrim mSysUiScrim;
+
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mActivity = StatefulActivity.fromContext(context);
@@ -89,6 +93,18 @@
         }
     }
 
+    public void setSysUiScrim(SysUiScrim scrim) {
+        mSysUiScrim = scrim;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mSysUiScrim != null) {
+            mSysUiScrim.draw(canvas);
+        }
+        super.dispatchDraw(canvas);
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
@@ -119,4 +135,4 @@
 
         void onWindowVisibilityChanged(int visibility);
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 8a45c81..2528c03 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -81,6 +81,7 @@
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
@@ -199,6 +200,9 @@
     private boolean mStripScreensOnPageStopMoving = false;
 
     private DragPreviewProvider mOutlineProvider = null;
+
+    private WorkspaceDragScrim mWorkspaceDragScrim;
+
     private boolean mWorkspaceFadeInAdjacentScreens;
 
     final WallpaperOffsetInterpolator mWallpaperOffset;
@@ -1161,6 +1165,19 @@
         }
     }
 
+    public void setWorkspaceDragScrim(WorkspaceDragScrim workspaceDragScrim) {
+        mWorkspaceDragScrim = workspaceDragScrim;
+    }
+
+    @Override
+    public void invalidate() {
+        // The workspace scrim may need to be re-rendered based on the workspace scroll
+        if (mWorkspaceDragScrim != null) {
+            mWorkspaceDragScrim.invalidate();
+        }
+        super.invalidate();
+    }
+
     @Override
     public void computeScroll() {
         super.computeScroll();
@@ -1706,10 +1723,9 @@
         if (dropOverView instanceof FolderIcon) {
             FolderIcon fi = (FolderIcon) dropOverView;
             if (fi.acceptDrop(d.dragInfo)) {
-                mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
-                        .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
+                mStatsLogManager.logger().withItemInfo(fi.mInfo).withInstanceId(d.logInstanceId)
+                        .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON);
                 fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
-
                 // if the drag started here, we need to remove it from the workspace
                 if (!external) {
                     getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
@@ -2028,7 +2044,7 @@
         }
         // Invalidating the scrim will also force this CellLayout
         // to be invalidated so that it is highlighted if necessary.
-        mLauncher.getDragLayer().getScrim().invalidate();
+        mLauncher.getDragLayer().getWorkspaceDragScrim().invalidate();
     }
 
     public CellLayout getCurrentDragOverlappingLayout() {
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index cd938e1..0e0ddfb 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -32,7 +32,7 @@
 import static com.android.launcher3.anim.Interpolators.ZOOM_OUT;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
-import static com.android.launcher3.graphics.WorkspaceAndHotseatScrim.SYSUI_PROGRESS;
+import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
@@ -49,7 +49,8 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
+import com.android.launcher3.graphics.SysUiScrim;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.DynamicResource;
 import com.android.systemui.plugins.ResourceProvider;
@@ -183,10 +184,12 @@
     }
 
     public void setScrim(PropertySetter propertySetter, LauncherState state) {
-        WorkspaceAndHotseatScrim scrim = mLauncher.getDragLayer().getScrim();
-        propertySetter.setFloat(scrim, SCRIM_PROGRESS, state.getWorkspaceScrimAlpha(mLauncher),
-                LINEAR);
-        propertySetter.setFloat(scrim, SYSUI_PROGRESS,
+        WorkspaceDragScrim workspaceDragScrim = mLauncher.getDragLayer().getWorkspaceDragScrim();
+        propertySetter.setFloat(workspaceDragScrim, SCRIM_PROGRESS,
+                state.getWorkspaceScrimAlpha(mLauncher), LINEAR);
+
+        SysUiScrim sysUiScrim = mLauncher.getDragLayer().getSysUiScrim();
+        propertySetter.setFloat(sysUiScrim, SYSUI_PROGRESS,
                 state.hasFlag(FLAG_HAS_SYS_UI_SCRIM) ? 1 : 0, LINEAR);
     }
 
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
index d51c786..748ba50 100644
--- a/src/com/android/launcher3/allapps/search/LiveSearchManager.java
+++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
@@ -153,11 +153,11 @@
             clearWidgetHost();
         }
 
-        if (finalState.equals(ALL_APPS)) {
+        if (ALL_APPS.equals(finalState)) {
             // creates new instance ID since new all apps session is started.
             mLogInstanceId = new InstanceIdSequence().newInstanceId();
             allAppsLogger().log(LAUNCHER_ALLAPPS_ENTRY);
-        } else if (mPrevLauncherState.equals(ALL_APPS)
+        } else if (ALL_APPS.equals(mPrevLauncherState)
                 // Check if mLogInstanceId is not null; to avoid NPE when LAUNCHER_ALLAPPS_EXIT is
                 // triggered multiple times
                 && mLogInstanceId != null) {
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 1cfe6ac..93df599 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -40,12 +40,14 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.TouchController;
 
 import java.util.ArrayList;
+import java.util.Optional;
 
 /**
  * Class for initiating a drag within a view or across multiple views.
@@ -230,6 +232,11 @@
         }
     }
 
+    public Optional<InstanceId> getLogInstanceId() {
+        return Optional.ofNullable(mDragObject)
+                .map(dragObject -> dragObject.logInstanceId);
+    }
+
     /**
      * Call this from a drag source view like this:
      *
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index e71c12d..7a6b4f9 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -41,12 +41,14 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTargetBar;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
+import com.android.launcher3.graphics.SysUiScrim;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.BaseDragLayer;
@@ -82,8 +84,10 @@
 
     // Related to adjacent page hints
     private final ViewGroupFocusHelper mFocusIndicatorHelper;
-    private final WorkspaceAndHotseatScrim mWorkspaceScrim;
     private final OverviewScrim mOverviewScrim;
+    private WorkspaceDragScrim mWorkspaceDragScrim;
+    private SysUiScrim mSysUiScrim;
+    private LauncherRootView mRootView;
 
     /**
      * Used to create a new DragLayer from XML.
@@ -99,14 +103,23 @@
         setChildrenDrawingOrderEnabled(true);
 
         mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
-        mWorkspaceScrim = new WorkspaceAndHotseatScrim(this);
         mOverviewScrim = new OverviewScrim(this);
     }
 
     public void setup(DragController dragController, Workspace workspace) {
         mDragController = dragController;
-        mWorkspaceScrim.setWorkspace(workspace);
         recreateControllers();
+
+        mWorkspaceDragScrim = new WorkspaceDragScrim((this));
+        mWorkspaceDragScrim.setWorkspace(workspace);
+
+        // We delegate drawing of the workspace scrim to LauncherRootView (one level up), so as
+        // to avoid artifacts when translating the entire drag layer in the -1 transition.
+        mRootView = (LauncherRootView) getParent();
+        mSysUiScrim = new SysUiScrim(mRootView);
+        mRootView.setSysUiScrim(mSysUiScrim);
+
+
     }
 
     @Override
@@ -515,7 +528,7 @@
     @Override
     protected void dispatchDraw(Canvas canvas) {
         // Draw the background below children.
-        mWorkspaceScrim.draw(canvas);
+        mWorkspaceDragScrim.draw(canvas);
         mOverviewScrim.updateCurrentScrimmedView(this);
         mFocusIndicatorHelper.draw(canvas);
         super.dispatchDraw(canvas);
@@ -535,18 +548,22 @@
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
-        mWorkspaceScrim.setSize(w, h);
+        mSysUiScrim.setSize(w, h);
     }
 
     @Override
     public void setInsets(Rect insets) {
         super.setInsets(insets);
-        mWorkspaceScrim.onInsetsChanged(insets, mAllowSysuiScrims);
+        mSysUiScrim.onInsetsChanged(insets, mAllowSysuiScrims);
         mOverviewScrim.onInsetsChanged(insets);
     }
 
-    public WorkspaceAndHotseatScrim getScrim() {
-        return mWorkspaceScrim;
+    public WorkspaceDragScrim getWorkspaceDragScrim() {
+        return mWorkspaceDragScrim;
+    }
+
+    public SysUiScrim getSysUiScrim() {
+        return mSysUiScrim;
     }
 
     public OverviewScrim getOverviewScrim() {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 61938d1..74d8dca 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -1208,7 +1208,9 @@
                         newIcon.requestFocus();
                     }
                     if (finalItem != null) {
-                        mStatsLogManager.logger().withItemInfo(finalItem)
+                        StatsLogger logger = mStatsLogManager.logger().withItemInfo(finalItem);
+                        mDragController.getLogInstanceId().map(logger::withInstanceId)
+                                .orElse(logger)
                                 .log(LAUNCHER_FOLDER_CONVERTED_TO_ICON);
                     }
                 }
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
similarity index 83%
rename from src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
rename to src/com/android/launcher3/graphics/SysUiScrim.java
index 7b7ab5e..d9c648b 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -34,7 +34,6 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.Region;
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
 import android.util.DisplayMetrics;
@@ -44,41 +43,39 @@
 
 import androidx.core.graphics.ColorUtils;
 
-import com.android.launcher3.CellLayout;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.util.Themes;
 
 /**
  * View scrim which draws behind hotseat and workspace
  */
-public class WorkspaceAndHotseatScrim extends Scrim {
+public class SysUiScrim extends Scrim {
 
-    public static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_PROGRESS =
-            new FloatProperty<WorkspaceAndHotseatScrim>("sysUiProgress") {
+    public static final FloatProperty<SysUiScrim> SYSUI_PROGRESS =
+            new FloatProperty<SysUiScrim>("sysUiProgress") {
                 @Override
-                public Float get(WorkspaceAndHotseatScrim scrim) {
+                public Float get(SysUiScrim scrim) {
                     return scrim.mSysUiProgress;
                 }
 
                 @Override
-                public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
+                public void setValue(SysUiScrim scrim, float value) {
                     scrim.setSysUiProgress(value);
                 }
             };
 
-    private static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_ANIM_MULTIPLIER =
-            new FloatProperty<WorkspaceAndHotseatScrim>("sysUiAnimMultiplier") {
+    private static final FloatProperty<SysUiScrim> SYSUI_ANIM_MULTIPLIER =
+            new FloatProperty<SysUiScrim>("sysUiAnimMultiplier") {
                 @Override
-                public Float get(WorkspaceAndHotseatScrim scrim) {
+                public Float get(SysUiScrim scrim) {
                     return scrim.mSysUiAnimMultiplier;
                 }
 
                 @Override
-                public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
+                public void setValue(SysUiScrim scrim, float value) {
                     scrim.mSysUiAnimMultiplier = value;
                     scrim.reapplySysUiAlpha();
                 }
@@ -108,10 +105,6 @@
     private static final int ALPHA_MASK_BITMAP_DP = 200;
     private static final int ALPHA_MASK_WIDTH_DP = 2;
 
-    private final Rect mHighlightRect = new Rect();
-
-    private Workspace mWorkspace;
-
     private boolean mDrawTopScrim, mDrawBottomScrim;
 
     private final RectF mFinalMaskRect = new RectF();
@@ -127,9 +120,8 @@
     private boolean mAnimateScrimOnNextDraw = false;
     private float mSysUiAnimMultiplier = 1;
 
-    public WorkspaceAndHotseatScrim(View view) {
+    public SysUiScrim(View view) {
         super(view);
-
         mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
                 view.getResources().getDisplayMetrics());
         mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
@@ -139,28 +131,10 @@
         onExtractedColorsChanged(mWallpaperColorInfo);
     }
 
-    public void setWorkspace(Workspace workspace)  {
-        mWorkspace = workspace;
-    }
-
+    /**
+     * Draw the top and bottom scrims
+     */
     public void draw(Canvas canvas) {
-        // Draw the background below children.
-        if (mScrimAlpha > 0) {
-            // Update the scroll position first to ensure scrim cutout is in the right place.
-            mWorkspace.computeScrollWithoutInvalidation();
-            CellLayout currCellLayout = mWorkspace.getCurrentDragOverlappingLayout();
-            canvas.save();
-            if (currCellLayout != null && currCellLayout != mLauncher.getHotseat()) {
-                // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
-                mLauncher.getDragLayer()
-                        .getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
-                canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
-            }
-
-            super.draw(canvas);
-            canvas.restore();
-        }
-
         if (!mHideSysUiScrim) {
             if (mSysUiProgress <= 0) {
                 mAnimateScrimOnNextDraw = false;
@@ -247,6 +221,11 @@
         super.onExtractedColorsChanged(wallpaperColorInfo);
     }
 
+    /**
+     * Set the width and height of the view being scrimmed
+     * @param w
+     * @param h
+     */
     public void setSize(int w, int h) {
         if (mTopScrim != null) {
             mTopScrim.setBounds(0, 0, w, h);
diff --git a/src/com/android/launcher3/graphics/WorkspaceDragScrim.java b/src/com/android/launcher3/graphics/WorkspaceDragScrim.java
new file mode 100644
index 0000000..d8dc563
--- /dev/null
+++ b/src/com/android/launcher3/graphics/WorkspaceDragScrim.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.graphics;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Workspace;
+
+/**
+ * Scrim drawn during SpringLoaded State (ie. Drag and Drop). Darkens the workspace except for
+ * the focused CellLayout.
+ */
+public class WorkspaceDragScrim extends Scrim {
+
+    private final Rect mHighlightRect = new Rect();
+
+    private Workspace mWorkspace;
+
+    public WorkspaceDragScrim(View view) {
+        super(view);
+        onExtractedColorsChanged(mWallpaperColorInfo);
+    }
+
+    /**
+     * Set the workspace that this scrim is acting on
+     * @param workspace
+     */
+    public void setWorkspace(Workspace workspace)  {
+        mWorkspace = workspace;
+        mWorkspace.setWorkspaceDragScrim(this);
+    }
+
+    /**
+     * Cut out the focused paged of the Workspace and then draw the scrim
+     * @param canvas
+     */
+    public void draw(Canvas canvas) {
+        // Draw the background below children.
+        if (mScrimAlpha > 0) {
+            // Update the scroll position first to ensure scrim cutout is in the right place.
+            mWorkspace.computeScrollWithoutInvalidation();
+            CellLayout currCellLayout = mWorkspace.getCurrentDragOverlappingLayout();
+            canvas.save();
+            if (currCellLayout != null && currCellLayout != mLauncher.getHotseat()) {
+                // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
+                mLauncher.getDragLayer()
+                        .getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
+                canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
+            }
+
+            super.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+}
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 071b263..cc80a88 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -95,9 +95,13 @@
         @UiEvent(doc = "User dragged a launcher item")
         LAUNCHER_ITEM_DRAG_STARTED(383),
 
-        @UiEvent(doc = "A dragged launcher item is successfully dropped")
+        @UiEvent(doc = "A dragged launcher item is successfully dropped onto workspace, hotseat "
+                + "open folder etc")
         LAUNCHER_ITEM_DROP_COMPLETED(385),
 
+        @UiEvent(doc = "A dragged launcher item is successfully dropped onto a folder icon.")
+        LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON(697),
+
         @UiEvent(doc = "A dragged launcher item is successfully dropped on another item "
                 + "resulting in a new folder creation")
         LAUNCHER_ITEM_DROP_FOLDER_CREATED(386),
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index d1e5017..543ee2a 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -28,7 +28,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -123,7 +123,7 @@
     }
 
     public void bindUpdatedWidgets(BgDataModel dataModel) {
-        final ArrayList<WidgetListRowEntry> widgets =
+        final ArrayList<WidgetsListBaseEntry> widgets =
                 dataModel.widgetsModel.getWidgetsList(mApp.getContext());
         scheduleCallbackTask(c -> c.bindAllWidgets(widgets));
     }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 2d860a4..1d7d1a2 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -50,7 +50,7 @@
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.ViewOnDrawExecutor;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -467,7 +467,7 @@
         void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
         void bindRestoreItemsChange(HashSet<ItemInfo> updates);
         void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
-        void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets);
+        void bindAllWidgets(List<WidgetsListBaseEntry> widgets);
         void onPageBoundSynchronously(int page);
         void executeOnNextDraw(ViewOnDrawExecutor executor);
         void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index cc783f7..cd2ef35 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -34,6 +34,7 @@
 import com.android.launcher3.folder.FolderNameInfos;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.Attribute;
+import com.android.launcher3.logger.LauncherAtom.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.model.ModelWriter;
@@ -208,8 +209,13 @@
 
     @Override
     public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
+        FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
+                .setCardinality(contents.size());
+        if (LabelState.SUGGESTED.equals(getLabelState())) {
+            folderIcon.setLabelInfo(title.toString());
+        }
         return getDefaultItemInfoBuilder()
-                .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
+                .setFolderIcon(folderIcon)
                 .setRank(rank)
                 .setAttribute(getLabelState().mLogAttribute)
                 .setContainerInfo(getContainerInfo())
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 76048ba..1853632 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -31,10 +31,10 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -59,7 +59,7 @@
     /** Maps packages to their DotInfo's . */
     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
     /** Maps packages to their Widgets */
-    private ArrayList<WidgetListRowEntry> mAllWidgets = new ArrayList<>();
+    private List<WidgetsListBaseEntry> mAllWidgets = List.of();
 
     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
 
@@ -187,7 +187,7 @@
         notificationListener.cancelNotificationFromLauncher(notificationKey);
     }
 
-    public void setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets) {
+    public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
         mAllWidgets = allWidgets;
         mChangeListener.onWidgetsBound();
     }
@@ -196,14 +196,15 @@
         mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
     }
 
-    public ArrayList<WidgetListRowEntry> getAllWidgets() {
+    public List<WidgetsListBaseEntry> getAllWidgets() {
         return mAllWidgets;
     }
 
     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
         return mAllWidgets.stream()
-                .filter(row -> row.pkgItem.packageName.equals(packageUserKey.mPackageName))
-                .flatMap(row -> row.widgets.stream())
+                .filter(row -> row instanceof WidgetsListContentEntry
+                        && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
+                .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
                 .filter(widget -> packageUserKey.mUser.equals(widget.user))
                 .collect(Collectors.toList());
     }
diff --git a/src/com/android/launcher3/recyclerview/ViewHolderBinder.java b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java
new file mode 100644
index 0000000..4653774
--- /dev/null
+++ b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java
@@ -0,0 +1,44 @@
+/*
+ * 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.recyclerview;
+
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * Creates and populates views with data
+ *
+ * @param <T> A data model which is used to populate the {@link ViewHolder}.
+ * @param <V> A subclass of {@link ViewHolder} which holds references to views.
+ */
+public interface ViewHolderBinder<T, V extends ViewHolder> {
+    /**
+     * Creates a new view, and attach it to the parent {@link ViewGroup}. Then, populates UI
+     * references in a {@link ViewHolder}.
+     */
+    V newViewHolder(ViewGroup parent);
+
+    /** Populate UI references in {@link ViewHolder} with data. */
+    void bindViewHolder(V viewHolder, T data);
+
+    /**
+     * Called when the view is recycled. Views are recycled in batches once they are sufficiently
+     * far off screen that it is unlikely the user will scroll back to them soon. Optionally
+     * override this to free expensive resources.
+     */
+    default void unbindViewHolder(V viewHolder) {}
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 0266345..0e8d4ae 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -46,7 +46,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -232,7 +232,7 @@
     public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
 
     @Override
-    public void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets) { }
+    public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
 
     @Override
     public void onPageBoundSynchronously(int page) { }
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index d4a132e..4261d08 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -33,7 +33,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.concurrent.ExecutionException;
 import java.util.function.Function;
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 10cd04c..25ecea5 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -543,7 +543,8 @@
             // case the user started interacting with it before the animation finished.
             mLauncher.getStateManager().goToState(targetState, false /* animated */);
         }
-        mLauncher.getDragLayer().getScrim().createSysuiMultiplierAnim(1f).setDuration(0).start();
+        mLauncher.getDragLayer().getSysUiScrim().createSysuiMultiplierAnim(
+                1f).setDuration(0).start();
     }
 
     private void logReachedState(LauncherState targetState) {
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 80f0981..addaf9c 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -49,7 +49,7 @@
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/com/android/launcher3/widget/WidgetListRowEntry.java b/src/com/android/launcher3/widget/WidgetListRowEntry.java
deleted file mode 100644
index 17e4673..0000000
--- a/src/com/android/launcher3/widget/WidgetListRowEntry.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import java.util.ArrayList;
-
-/**
- * Holder class to store all the information related to a single row in the widget list
- */
-public class WidgetListRowEntry {
-
-    public final PackageItemInfo pkgItem;
-
-    public final ArrayList<WidgetItem> widgets;
-
-    /**
-     * Character that is used as a section name for the {@link ItemInfo#title}.
-     * (e.g., "G" will be stored if title is "Google")
-     */
-    public String titleSectionName;
-
-    public WidgetListRowEntry(PackageItemInfo pkgItem, ArrayList<WidgetItem> items) {
-        this.pkgItem = pkgItem;
-        this.widgets = items;
-    }
-
-    @Override
-    public String toString() {
-        return pkgItem.packageName + ":" + widgets.size();
-    }
-}
diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java
deleted file mode 100644
index 5bf9690..0000000
--- a/src/com/android/launcher3/widget/WidgetsListAdapter.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2015 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;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-import android.view.ViewGroup;
-
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-
-import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.LabelComparator;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * List 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. {@link #getItemViewType} was not overridden as there is
- * only a single type of view.
- */
-public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> {
-
-    private static final String TAG = "WidgetsListAdapter";
-    private static final boolean DEBUG = false;
-
-    private final WidgetPreviewLoader mWidgetPreviewLoader;
-    private final LayoutInflater mLayoutInflater;
-
-    private final OnClickListener mIconClickListener;
-    private final OnLongClickListener mIconLongClickListener;
-    private final int mIndent;
-    private ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
-    private final WidgetsDiffReporter mDiffReporter;
-
-    private boolean mApplyBitmapDeferred;
-
-    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
-            WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
-            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
-        mLayoutInflater = layoutInflater;
-        mWidgetPreviewLoader = widgetPreviewLoader;
-        mIconClickListener = iconClickListener;
-        mIconLongClickListener = iconLongClickListener;
-        mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
-        mDiffReporter = new WidgetsDiffReporter(iconCache, this);
-    }
-
-    /**
-     * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}
-     *
-     * @see WidgetCell#setApplyBitmapDeferred(boolean)
-     */
-    public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
-        mApplyBitmapDeferred = isDeferred;
-
-        for (int i = rv.getChildCount() - 1; i >= 0; i--) {
-            WidgetsRowViewHolder holder = (WidgetsRowViewHolder)
-                    rv.getChildViewHolder(rv.getChildAt(i));
-            for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) {
-                View v = holder.cellContainer.getChildAt(j);
-                if (v instanceof WidgetCell) {
-                    ((WidgetCell) v).setApplyBitmapDeferred(mApplyBitmapDeferred);
-                }
-            }
-        }
-    }
-
-    /**
-     * Update the widget list.
-     */
-    public void setWidgets(ArrayList<WidgetListRowEntry> tempEntries) {
-        WidgetListRowEntryComparator rowComparator = new WidgetListRowEntryComparator();
-        Collections.sort(tempEntries, rowComparator);
-        mDiffReporter.process(mEntries, tempEntries, rowComparator);
-    }
-
-    @Override
-    public int getItemCount() {
-        return mEntries.size();
-    }
-
-    public String getSectionName(int pos) {
-        return mEntries.get(pos).titleSectionName;
-    }
-
-    @Override
-    public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) {
-        WidgetListRowEntry entry = mEntries.get(pos);
-        List<WidgetItem> infoList = entry.widgets;
-
-        ViewGroup row = holder.cellContainer;
-        if (DEBUG) {
-            Log.d(TAG, String.format(
-                    "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]",
-                    pos, infoList.size(), row.getChildCount()));
-        }
-
-        // Add more views.
-        // if there are too many, hide them.
-        int expectedChildCount = infoList.size() + Math.max(0, infoList.size() - 1);
-        int childCount = row.getChildCount();
-
-        if (expectedChildCount > childCount) {
-            for (int i = childCount; i < expectedChildCount; i++) {
-                if ((i & 1) == 1) {
-                    // Add a divider for odd index
-                    mLayoutInflater.inflate(R.layout.widget_list_divider, row);
-                } else {
-                    // Add cell for even index
-                    WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
-                            R.layout.widget_cell, row, false);
-
-                    // set up touch.
-                    widget.setOnClickListener(mIconClickListener);
-                    widget.setOnLongClickListener(mIconLongClickListener);
-                    row.addView(widget);
-                }
-            }
-        } else if (expectedChildCount < childCount) {
-            for (int i = expectedChildCount; i < childCount; i++) {
-                row.getChildAt(i).setVisibility(View.GONE);
-            }
-        }
-
-        // Bind the views in the application info section.
-        holder.title.applyFromItemInfoWithIcon(entry.pkgItem);
-
-        // Bind the view in the widget horizontal tray region.
-        for (int i = 0; i < infoList.size(); i++) {
-            WidgetCell widget = (WidgetCell) row.getChildAt(2 * i);
-            widget.applyFromCellItem(infoList.get(i), mWidgetPreviewLoader);
-            widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
-            widget.ensurePreview();
-            widget.setVisibility(View.VISIBLE);
-
-            if (i > 0) {
-                row.getChildAt(2 * i - 1).setVisibility(View.VISIBLE);
-            }
-        }
-    }
-
-    @Override
-    public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        if (DEBUG) {
-            Log.v(TAG, "\nonCreateViewHolder");
-        }
-
-        ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
-                R.layout.widgets_list_row_view, 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.
-        container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0);
-
-        return new WidgetsRowViewHolder(container);
-    }
-
-    @Override
-    public void onViewRecycled(WidgetsRowViewHolder holder) {
-        int total = holder.cellContainer.getChildCount();
-        for (int i = 0; i < total; i += 2) {
-            WidgetCell widget = (WidgetCell) holder.cellContainer.getChildAt(i);
-            widget.clear();
-        }
-    }
-
-    public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) {
-        // If child views are animating, then the RecyclerView may choose not to recycle the view,
-        // causing extraneous onCreateViewHolder() calls.  It is safe in this case to continue
-        // recycling this view, and take care in onViewRecycled() to cancel any existing
-        // animations.
-        return true;
-    }
-
-    @Override
-    public long getItemId(int pos) {
-        return pos;
-    }
-
-    /**
-     * Comparator for sorting WidgetListRowEntry based on package title
-     */
-    public static class WidgetListRowEntryComparator implements Comparator<WidgetListRowEntry> {
-
-        private final LabelComparator mComparator = new LabelComparator();
-
-        @Override
-        public int compare(WidgetListRowEntry a, WidgetListRowEntry b) {
-            return mComparator.compare(a.pkgItem.title.toString(), b.pkgItem.title.toString());
-        }
-    }
-}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
new file mode 100644
index 0000000..10ea7db
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -0,0 +1,36 @@
+/*
+ * 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.data.ItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+/** Holder class to store the package information of an entry shown in the widgets list. */
+public abstract class WidgetsListBaseEntry {
+    public final PackageItemInfo mPkgItem;
+
+    /**
+     * Character that is used as a section name for the {@link ItemInfo#title}.
+     * (e.g., "G" will be stored if title is "Google")
+     */
+    public final String mTitleSectionName;
+
+    public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName) {
+        mPkgItem = pkgItem;
+        mTitleSectionName = titleSectionName;
+    }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
new file mode 100644
index 0000000..407f194
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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 com.android.launcher3.widget.WidgetItemComparator;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Holder class to store all the information related to a list of widgets from the same app which is
+ * shown in the {@link com.android.launcher3.widget.picker.WidgetsFullSheet}.
+ */
+public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
+
+    public final List<WidgetItem> mWidgets;
+
+    public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
+            List<WidgetItem> items) {
+        super(pkgItem, titleSectionName);
+        this.mWidgets =
+                items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
+    }
+
+    @Override
+    public String toString() {
+        return mPkgItem.packageName + ":" + mWidgets.size();
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
similarity index 65%
rename from src/com/android/launcher3/widget/WidgetsDiffReporter.java
rename to src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
index df6e2c3..398d9ba 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
 
 import android.util.Log;
 
@@ -22,7 +22,9 @@
 
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -43,8 +45,13 @@
         mListener = listener;
     }
 
-    public void process(ArrayList<WidgetListRowEntry> currentEntries,
-            ArrayList<WidgetListRowEntry> newEntries, WidgetListRowEntryComparator comparator) {
+    /**
+     * Notifies the difference between {@code currentEntries} & {@code newEntries} by calling the
+     * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods.
+     */
+    public void process(ArrayList<WidgetsListBaseEntry> currentEntries,
+            ArrayList<WidgetsListBaseEntry> newEntries,
+            WidgetListBaseRowEntryComparator comparator) {
         if (DEBUG) {
             Log.d(TAG, "process oldEntries#=" + currentEntries.size()
                     + " newEntries#=" + newEntries.size());
@@ -62,20 +69,20 @@
             }
             return;
         }
-        ArrayList<WidgetListRowEntry> orgEntries =
-                (ArrayList<WidgetListRowEntry>) currentEntries.clone();
-        Iterator<WidgetListRowEntry> orgIter = orgEntries.iterator();
-        Iterator<WidgetListRowEntry> newIter = newEntries.iterator();
+        ArrayList<WidgetsListBaseEntry> orgEntries =
+                (ArrayList<WidgetsListBaseEntry>) currentEntries.clone();
+        Iterator<WidgetsListBaseEntry> orgIter = orgEntries.iterator();
+        Iterator<WidgetsListBaseEntry> newIter = newEntries.iterator();
 
-        WidgetListRowEntry orgRowEntry = orgIter.next();
-        WidgetListRowEntry newRowEntry = newIter.next();
+        WidgetsListBaseEntry orgRowEntry = orgIter.next();
+        WidgetsListBaseEntry newRowEntry = newIter.next();
 
         do {
             int diff = comparePackageName(orgRowEntry, newRowEntry, comparator);
             if (DEBUG) {
                 Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
-                        diff, orgRowEntry != null? orgRowEntry.toString() : null,
-                        newRowEntry != null? newRowEntry.toString() : null));
+                        diff, orgRowEntry != null ? orgRowEntry.toString() : null,
+                        newRowEntry != null ? newRowEntry.toString() : null));
             }
             int index = -1;
             if (diff < 0) {
@@ -83,17 +90,17 @@
                 mListener.notifyItemRemoved(index);
                 if (DEBUG) {
                     Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
-                            orgRowEntry.titleSectionName));
+                            orgRowEntry.mTitleSectionName));
                 }
                 currentEntries.remove(index);
                 orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
             } else if (diff > 0) {
-                index = orgRowEntry != null? currentEntries.indexOf(orgRowEntry):
-                        currentEntries.size();
+                index = orgRowEntry != null ? currentEntries.indexOf(orgRowEntry)
+                        : currentEntries.size();
                 currentEntries.add(index, newRowEntry);
                 if (DEBUG) {
                     Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
-                            newRowEntry.titleSectionName));
+                            newRowEntry.mTitleSectionName));
                 }
                 newRowEntry = newIter.hasNext() ? newIter.next() : null;
                 mListener.notifyItemInserted(index);
@@ -102,14 +109,14 @@
                 // same package name but,
                 // did the icon, title, etc, change?
                 // or did the widget size and desc, span, etc change?
-                if (!isSamePackageItemInfo(orgRowEntry.pkgItem, newRowEntry.pkgItem) ||
-                        !orgRowEntry.widgets.equals(newRowEntry.widgets)) {
+                if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
+                        || !areWidgetsEqual(orgRowEntry, newRowEntry)) {
                     index = currentEntries.indexOf(orgRowEntry);
                     currentEntries.set(index, newRowEntry);
                     mListener.notifyItemChanged(index);
                     if (DEBUG) {
                         Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
-                                newRowEntry.titleSectionName));
+                                newRowEntry.mTitleSectionName));
                     }
                 }
                 orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
@@ -122,10 +129,11 @@
      * Compare package name using the same comparator as in {@link WidgetsListAdapter}.
      * Also handle null row pointers.
      */
-    private int comparePackageName(WidgetListRowEntry curRow, WidgetListRowEntry newRow,
-            WidgetListRowEntryComparator comparator) {
+    private int comparePackageName(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow,
+            WidgetListBaseRowEntryComparator comparator) {
         if (curRow == null && newRow == null) {
-            throw new IllegalStateException("Cannot compare PackageItemInfo if both rows are null.");
+            throw new IllegalStateException(
+                    "Cannot compare PackageItemInfo if both rows are null.");
         }
 
         if (curRow == null && newRow != null) {
@@ -136,6 +144,17 @@
         return comparator.compare(curRow, newRow);
     }
 
+    private boolean areWidgetsEqual(WidgetsListBaseEntry curRow,
+            WidgetsListBaseEntry newRow) {
+        if (!(curRow instanceof WidgetsListContentEntry)
+                || !(newRow instanceof WidgetsListContentEntry)) {
+            return false;
+        }
+        WidgetsListContentEntry orgRowEntry = (WidgetsListContentEntry) curRow;
+        WidgetsListContentEntry newRowEntry = (WidgetsListContentEntry) newRow;
+        return orgRowEntry.mWidgets.equals(newRowEntry.mWidgets);
+    }
+
     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
         return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
                 && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
similarity index 95%
rename from src/com/android/launcher3/widget/WidgetsFullSheet.java
rename to src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 4c8c339..03623d5 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
@@ -42,6 +42,7 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
+import com.android.launcher3.widget.BaseWidgetSheet;
 
 /**
  * Popup for showing the full list of available widgets
@@ -218,8 +219,8 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mNoIntercept = false;
             RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar();
-            if (scroller.getThumbOffsetY() >= 0 &&
-                    getPopupContainer().isEventOverView(scroller, ev)) {
+            if (scroller.getThumbOffsetY() >= 0
+                    && getPopupContainer().isEventOverView(scroller, ev)) {
                 mNoIntercept = true;
             } else if (getPopupContainer().isEventOverView(mContent, ev)) {
                 mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, getPopupContainer());
@@ -228,6 +229,7 @@
         return super.onControllerInterceptTouchEvent(ev);
     }
 
+    /** Shows the {@link WidgetsFullSheet} on the launcher. */
     public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
         WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
                 .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
@@ -237,6 +239,7 @@
         return sheet;
     }
 
+    /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
     @VisibleForTesting
     public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
         return launcher.findViewById(R.id.widgets_list_view);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
new file mode 100644
index 0000000..9d30842
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2015 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.content.Context;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.LabelComparator;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * List 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.
+ */
+public class WidgetsListAdapter extends Adapter<ViewHolder> {
+
+    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 final WidgetsDiffReporter mDiffReporter;
+    private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
+    private final WidgetsListRowViewHolderBinder mWidgetsListRowViewHolderBinder;
+
+    private ArrayList<WidgetsListBaseEntry> mEntries = new ArrayList<>();
+
+    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
+            WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
+            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
+        mDiffReporter = new WidgetsDiffReporter(iconCache, this);
+        mWidgetsListRowViewHolderBinder = new WidgetsListRowViewHolderBinder(context,
+                layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader);
+        mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListRowViewHolderBinder);
+    }
+
+    /**
+     * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}.
+     *
+     * @see WidgetCell#setApplyBitmapDeferred(boolean)
+     */
+    public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
+        mWidgetsListRowViewHolderBinder.setApplyBitmapDeferred(isDeferred);
+
+        for (int i = rv.getChildCount() - 1; i >= 0; i--) {
+            ViewHolder viewHolder = rv.getChildViewHolder(rv.getChildAt(i));
+            if (viewHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
+                WidgetsRowViewHolder holder = (WidgetsRowViewHolder) viewHolder;
+                for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) {
+                    View v = holder.cellContainer.getChildAt(j);
+                    if (v instanceof WidgetCell) {
+                        ((WidgetCell) v).setApplyBitmapDeferred(isDeferred);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return mEntries.size();
+    }
+
+    /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
+    public String getSectionName(int pos) {
+        return mEntries.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);
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int pos) {
+        ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
+        viewHolderBinder.bindViewHolder(holder, mEntries.get(pos));
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        if (DEBUG) {
+            Log.v(TAG, "\nonCreateViewHolder");
+        }
+
+        return mViewHolderBinders.get(viewType).newViewHolder(parent);
+    }
+
+    @Override
+    public void onViewRecycled(ViewHolder holder) {
+        mViewHolderBinders.get(holder.getItemViewType()).unbindViewHolder(holder);
+    }
+
+    @Override
+    public boolean onFailedToRecycleView(ViewHolder holder) {
+        // If child views are animating, then the RecyclerView may choose not to recycle the view,
+        // causing extraneous onCreateViewHolder() calls.  It is safe in this case to continue
+        // recycling this view, and take care in onViewRecycled() to cancel any existing
+        // animations.
+        return true;
+    }
+
+    @Override
+    public long getItemId(int pos) {
+        return pos;
+    }
+
+    @Override
+    public int getItemViewType(int pos) {
+        WidgetsListBaseEntry entry = mEntries.get(pos);
+        if (entry instanceof WidgetsListContentEntry) {
+            return VIEW_TYPE_WIDGETS_LIST;
+        }
+        throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
+    }
+
+    /** Comparator for sorting WidgetListRowEntry based on package title. */
+    public static class WidgetListBaseRowEntryComparator implements
+            Comparator<WidgetsListBaseEntry> {
+
+        private final LabelComparator mComparator = new LabelComparator();
+
+        @Override
+        public int compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b) {
+            return mComparator.compare(a.mPkgItem.title.toString(), b.mPkgItem.title.toString());
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java
new file mode 100644
index 0000000..22a8d00
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java
@@ -0,0 +1,150 @@
+/*
+ * 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.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import java.util.List;
+
+/**
+ * Binds data from {@link WidgetsListContentEntry} to UI elements in {@link WidgetsRowViewHolder}.
+ */
+public class WidgetsListRowViewHolderBinder
+        implements ViewHolderBinder<WidgetsListContentEntry, WidgetsRowViewHolder> {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "WidgetsListRowViewHolderBinder";
+
+    private final LayoutInflater mLayoutInflater;
+    private final int mIndent;
+    private final OnClickListener mIconClickListener;
+    private final OnLongClickListener mIconLongClickListener;
+    private final WidgetPreviewLoader mWidgetPreviewLoader;
+    private boolean mApplyBitmapDeferred = false;
+
+    public WidgetsListRowViewHolderBinder(
+            Context context,
+            LayoutInflater layoutInflater,
+            OnClickListener iconClickListener,
+            OnLongClickListener iconLongClickListener,
+            WidgetPreviewLoader widgetPreviewLoader) {
+        mLayoutInflater = layoutInflater;
+        mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+        mWidgetPreviewLoader = widgetPreviewLoader;
+    }
+
+    /**
+     * Defers applying bitmap on all the {@link WidgetCell} at
+     * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if
+     * {@code applyBitmapDeferred} is {@code true}.
+     */
+    public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
+        mApplyBitmapDeferred = applyBitmapDeferred;
+    }
+
+    @Override
+    public WidgetsRowViewHolder newViewHolder(ViewGroup parent) {
+        if (DEBUG) {
+            Log.v(TAG, "\nonCreateViewHolder");
+        }
+
+        ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_view, 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.
+        container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0);
+
+        return new WidgetsRowViewHolder(container);
+    }
+
+    @Override
+    public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry) {
+        List<WidgetItem> infoList = entry.mWidgets;
+
+        ViewGroup row = holder.cellContainer;
+        if (DEBUG) {
+            Log.d(TAG, String.format("onBindViewHolder [widget#=%d, row.getChildCount=%d]",
+                    infoList.size(), row.getChildCount()));
+        }
+
+        // Add more views.
+        // if there are too many, hide them.
+        int expectedChildCount = infoList.size() + Math.max(0, infoList.size() - 1);
+        int childCount = row.getChildCount();
+
+        if (expectedChildCount > childCount) {
+            for (int i = childCount; i < expectedChildCount; i++) {
+                if ((i & 1) == 1) {
+                    // Add a divider for odd index
+                    mLayoutInflater.inflate(R.layout.widget_list_divider, row);
+                } else {
+                    // Add cell for even index
+                    WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
+                            R.layout.widget_cell, row, false);
+
+                    // set up touch.
+                    widget.setOnClickListener(mIconClickListener);
+                    widget.setOnLongClickListener(mIconLongClickListener);
+                    row.addView(widget);
+                }
+            }
+        } else if (expectedChildCount < childCount) {
+            for (int i = expectedChildCount; i < childCount; i++) {
+                row.getChildAt(i).setVisibility(View.GONE);
+            }
+        }
+
+        // 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);
+            widget.applyFromCellItem(infoList.get(i), mWidgetPreviewLoader);
+            widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
+            widget.ensurePreview();
+            widget.setVisibility(View.VISIBLE);
+
+            if (i > 0) {
+                row.getChildAt(2 * i - 1).setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void unbindViewHolder(WidgetsRowViewHolder holder) {
+        int total = holder.cellContainer.getChildCount();
+        for (int i = 0; i < total; i += 2) {
+            WidgetCell widget = (WidgetCell) holder.cellContainer.getChildAt(i);
+            widget.clear();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
similarity index 97%
rename from src/com/android/launcher3/widget/WidgetsRecyclerView.java
rename to src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index 69de12b..52e9496 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
 
 import android.content.Context;
 import android.graphics.Point;
@@ -22,13 +22,13 @@
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.launcher3.BaseRecyclerView;
-import com.android.launcher3.R;
-
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.R;
+
 /**
  * The widgets recycler view.
  */
@@ -89,7 +89,7 @@
         LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
         layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
 
-        int posInt = (int) ((touchFraction == 1)? pos -1 : pos);
+        int posInt = (int) ((touchFraction == 1) ? pos - 1 : pos);
         return mAdapter.getSectionName(posInt);
     }
 
@@ -171,4 +171,4 @@
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
similarity index 86%
rename from src/com/android/launcher3/widget/WidgetsRowViewHolder.java
rename to src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
index d26edb6..9be079e 100644
--- a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
@@ -13,16 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
 
 import android.view.ViewGroup;
 
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
 
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-public class WidgetsRowViewHolder extends ViewHolder {
+/** A {@link ViewHolder} for a row in the full widget picker. */
+public final class WidgetsRowViewHolder extends ViewHolder {
 
     public final ViewGroup cellContainer;
     public final BubbleTextView title;
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 269af7b..73b1601 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -21,10 +21,10 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
-import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -47,7 +47,7 @@
 
     @Override
     public void bindWidgets() {
-        final ArrayList<WidgetListRowEntry> widgets =
+        final List<WidgetsListBaseEntry> widgets =
                 mBgDataModel.widgetsModel.getWidgetsList(mApp.getContext());
         executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
     }
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index b4e45f8..f27922b 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -28,12 +28,12 @@
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.widget.WidgetItemComparator;
-import com.android.launcher3.widget.WidgetListRowEntry;
 import com.android.launcher3.widget.WidgetManagerHelper;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.picker.WidgetsDiffReporter;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -60,24 +60,23 @@
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
 
     /**
-     * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
-     * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
-     * is not sorted. This list is sorted at the UI when using
-     * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+     * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
+     * are sorted (based on label and user), but the overall list of
+     * {@link WidgetsListBaseEntry}s is not sorted. This list is sorted at the UI when using
+     * {@link WidgetsDiffReporter}
      *
-     * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
      */
-    public synchronized ArrayList<WidgetListRowEntry> getWidgetsList(Context context) {
-        ArrayList<WidgetListRowEntry> result = new ArrayList<>();
+    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsList(Context context) {
+        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
         AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
 
-        WidgetItemComparator widgetComparator = new WidgetItemComparator();
         for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
-            WidgetListRowEntry row = new WidgetListRowEntry(
-                    entry.getKey(), new ArrayList<>(entry.getValue()));
-            row.titleSectionName = (row.pkgItem.title == null) ? "" :
-                    indexer.computeSectionName(row.pkgItem.title);
-            Collections.sort(row.widgets, widgetComparator);
+            PackageItemInfo pkgItem = entry.getKey();
+            String sectionName = (pkgItem.title == null) ? "" :
+                    indexer.computeSectionName(pkgItem.title);
+            WidgetsListContentEntry row =
+                    new WidgetsListContentEntry(pkgItem, sectionName, entry.getValue());
             result.add(row);
         }
         return result;
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 1648bdd..4c9a8e7 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -37,8 +37,8 @@
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.views.OptionsPopupView;
-import com.android.launcher3.widget.WidgetsFullSheet;
-import com.android.launcher3.widget.WidgetsRecyclerView;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
 import org.junit.Before;
 import org.junit.Ignore;