Renders widget recommendations

Update the tapl test logic to scroll a smaller distance to avoid the
search bar blocking the target touch area.

Test: Open full widgets sheet and observe the widget recommendations
      shown at the top.
      Run AddConfigWidgetTest

Bug: 179797520

Change-Id: I6d53bbb46e2cb928ed7d015aaac604be17d33178
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 7b85d9b..a3d0070 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -18,10 +18,11 @@
     android:layout_height="wrap_content">
 
     <!-- The image of the widget. This view does not support padding. Any placement adjustment
-         should be done using margins. -->
+         should be done using margins.
+          width & height are set at runtime after scaling the preview image. -->
     <com.android.launcher3.widget.WidgetImageView
         android:id="@+id/widget_preview"
-        android:layout_width="wrap_content"
+        android:layout_width="0dp"
         android:layout_height="0dp"
         android:layout_weight="1"
         android:importantForAccessibility="no"
@@ -38,7 +39,7 @@
         android:singleLine="true"
         android:maxLines="1"
         android:textColor="?android:attr/textColorPrimary"
-        android:textSize="14sp" />
+        android:textSize="@dimen/widget_cell_font_size" />
 
     <!-- The original dimensions of the widget (can't be the same text as above due to different
          style. -->
@@ -48,7 +49,7 @@
         android:layout_height="wrap_content"
         android:gravity="center_horizontal"
         android:textColor="?android:attr/textColorTertiary"
-        android:textSize="14sp"
+        android:textSize="@dimen/widget_cell_font_size"
         android:alpha="0.8" />
 
     <TextView
@@ -56,7 +57,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:gravity="center_horizontal"
-        android:textSize="14sp"
+        android:textSize="@dimen/widget_cell_font_size"
         android:textColor="?android:attr/textColorTertiary"
         android:maxLines="2"
         android:ellipsize="end"
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
index 1219f57..e5df175 100644
--- a/res/layout/widgets_full_sheet_search_and_recommendations.xml
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -18,12 +18,15 @@
     android:id="@+id/search_and_recommendations_container"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="16dp"
+    android:paddingHorizontal="16dp"
+    android:layout_marginBottom="16dp"
     android:orientation="vertical">
     <View
         android:id="@+id/collapse_handle"
         android:layout_width="48dp"
         android:layout_height="2dp"
+        android:layout_marginTop="16dp"
+        android:elevation="2dp"
         android:layout_gravity="center_horizontal"
         android:background="?android:attr/textColorSecondary"/>
     <TextView
@@ -36,4 +39,11 @@
         android:textColor="?android:attr/textColorSecondary"
         android:text="@string/widget_button_text"/>
     <include layout="@layout/widgets_search_bar"/>
+
+    <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
+        android:id="@+id/recommended_widget_table"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:visibility="gone" />
 </LinearLayout>
diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml
index 021058b..1db7462 100644
--- a/res/layout/widgets_search_bar.xml
+++ b/res/layout/widgets_search_bar.xml
@@ -6,7 +6,8 @@
     android:layout_height="wrap_content"
     android:orientation="horizontal"
     android:layout_marginTop="16dp"
-    android:background="@drawable/bg_widgets_searchbox">
+    android:background="@drawable/bg_widgets_searchbox"
+    android:elevation="2dp">
 
     <com.android.launcher3.ExtendedEditText
         android:id="@+id/widgets_search_bar_edit_text"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c607c98..aa5a09c 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -109,6 +109,7 @@
 <!-- Widget tray -->
     <dimen name="widget_cell_vertical_padding">8dp</dimen>
     <dimen name="widget_cell_horizontal_padding">16dp</dimen>
+    <dimen name="widget_cell_font_size">14sp</dimen>
 
 
     <dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 5328041..1b0e1ce 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -74,6 +74,7 @@
     protected int mPreviewHeight;
     protected int mPresetPreviewSize;
     private int mCellSize;
+    private float mPreviewScale = 1f;
 
     private WidgetImageView mWidgetImage;
     private TextView mWidgetName;
@@ -254,8 +255,8 @@
         }
         if (drawable != null) {
             LayoutParams layoutParams = (LayoutParams) mWidgetImage.getLayoutParams();
-            layoutParams.width = drawable.getIntrinsicWidth();
-            layoutParams.height = drawable.getIntrinsicHeight();
+            layoutParams.width = (int) (drawable.getIntrinsicWidth() * mPreviewScale);
+            layoutParams.height = (int) (drawable.getIntrinsicHeight() * mPreviewScale);
             mWidgetImage.setLayoutParams(layoutParams);
 
             mWidgetImage.setDrawable(drawable, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
@@ -305,10 +306,16 @@
 
     /** Sets the widget preview image size in number of cells. */
     public void setPreviewSize(int spanX, int spanY) {
+        setPreviewSize(spanX, spanY, 1f);
+    }
+
+    /** Sets the widget preview image size, in number of cells, and preview scale. */
+    public void setPreviewSize(int spanX, int spanY, float previewScale) {
         int padding = 2 * getResources()
                 .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
         mPreviewWidth = mDeviceProfile.cellWidthPx * spanX + padding;
         mPreviewHeight = mDeviceProfile.cellHeightPx * spanY + padding;
+        mPreviewScale = previewScale;
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
index 7eb5b83..7f84077 100644
--- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -35,6 +35,7 @@
     private final SearchAndRecommendationViewHolder mViewHolder;
     private final WidgetsRecyclerView mPrimaryRecyclerView;
     private final WidgetsRecyclerView mSearchRecyclerView;
+    private final int mTabsHeight;
 
     // The following are only non null if mHasWorkProfile is true.
     @Nullable private final WidgetsRecyclerView mWorkRecyclerView;
@@ -42,10 +43,28 @@
     @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager;
 
     private WidgetsRecyclerView mCurrentRecyclerView;
-    private int mMaxCollapsibleHeight = 0;
+
+    /**
+     * The vertical distance, in pixels, until the search is pinned at the top of the screen when
+     * the user scrolls down the recycler view.
+     */
+    private int mCollapsibleHeightForSearch = 0;
+    /**
+     * The vertical distance, in pixels, until the recommendation table disappears from the top of
+     * the screen when the user scrolls down the recycler view.
+     */
+    private int mCollapsibleHeightForRecommendation = 0;
+    /**
+     * The vertical distance, in pixels, until the tabs is pinned at the top of the screen when the
+     * user scrolls down the recycler view.
+     *
+     * <p>Always 0 if there is no work profile.
+     */
+    private int mCollapsibleHeightForTabs = 0;
 
     SearchAndRecommendationsScrollController(
             boolean hasWorkProfile,
+            int tabsHeight,
             SearchAndRecommendationViewHolder viewHolder,
             WidgetsRecyclerView primaryRecyclerView,
             @Nullable WidgetsRecyclerView workRecyclerView,
@@ -55,46 +74,63 @@
         mHasWorkProfile = hasWorkProfile;
         mViewHolder = viewHolder;
         mPrimaryRecyclerView = primaryRecyclerView;
+        mCurrentRecyclerView = mPrimaryRecyclerView;
         mWorkRecyclerView = workRecyclerView;
         mSearchRecyclerView = searchRecyclerView;
         mPrimaryWorkTabsView = personalWorkTabsView;
         mPrimaryWorkViewPager = primaryWorkViewPager;
         mCurrentRecyclerView = mPrimaryRecyclerView;
+        mTabsHeight = tabsHeight;
     }
 
     /** Sets the current active {@link WidgetsRecyclerView}. */
     public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
         mCurrentRecyclerView = currentRecyclerView;
+        mCurrentRecyclerView = currentRecyclerView;
+        mViewHolder.mHeaderTitle.setTranslationY(0);
+        mViewHolder.mRecommendedWidgetsTable.setTranslationY(0);
+        mViewHolder.mSearchBar.setTranslationY(0);
+
+        if (mHasWorkProfile) {
+            mPrimaryWorkTabsView.setTranslationY(0);
+        }
     }
 
     /**
      * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views.
+     *
+     * @return {@code true} if margins or/and padding of views in the search and recommendations
+     * container have been updated.
      */
-    public void updateMarginAndPadding() {
-        // The maximum vertical distance, in pixels, until the last collapsible element is not
-        // visible from the screen when the user scrolls down the recycler view.
-        mMaxCollapsibleHeight = mViewHolder.mContainer.getPaddingTop()
-                + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle)
-                + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle);
+    public boolean updateMarginAndPadding() {
+        boolean hasMarginOrPaddingUpdated = false;
+        mCollapsibleHeightForSearch = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle);
+        mCollapsibleHeightForRecommendation =
+                measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle)
+                        + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle)
+                        + measureHeightWithVerticalMargins((View) mViewHolder.mSearchBar)
+                        + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable);
 
         int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer);
         if (mHasWorkProfile) {
+            mCollapsibleHeightForTabs = measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle)
+                    + measureHeightWithVerticalMargins(mViewHolder.mRecommendedWidgetsTable);
             // In a work profile setup, the full widget sheet contains the following views:
-            //           -------               -|
-            //           Widgets               -|---> LinearLayout for search & recommendations
-            //          Search bar             -|
-            //      Personal | Work
+            //           ------- (pinned)           -|
+            //          Widgets (collapsible)       -|---> LinearLayout for search & recommendations
+            //          Search bar (pinned)         -|
+            //  Widgets recommendation (collapsible)-|
+            //      Personal | Work (pinned)
             //           View Pager
             //
             // Views after the search & recommendations are not bound by RelativelyLayout param.
             // To position them on the expected location, padding & margin are added to these views
 
             // Tabs should have a padding of the height of the search & recommendations container.
-            mPrimaryWorkTabsView.setPadding(
-                    mPrimaryWorkTabsView.getPaddingLeft(),
-                    topContainerHeight,
-                    mPrimaryWorkTabsView.getPaddingRight(),
-                    mPrimaryWorkTabsView.getPaddingBottom());
+            RelativeLayout.LayoutParams tabsLayoutParams =
+                    (RelativeLayout.LayoutParams) mPrimaryWorkTabsView.getLayoutParams();
+            tabsLayoutParams.topMargin = topContainerHeight;
+            mPrimaryWorkTabsView.setLayoutParams(tabsLayoutParams);
 
             // Instead of setting the top offset directly, we split the top offset into two values:
             // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible
@@ -124,39 +160,52 @@
             //
             // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and
             // mMaxCollapsibleDistance should equal to the top container height.
-            int tabsViewActualHeight = measureHeightWithVerticalMargins(mPrimaryWorkTabsView)
-                    - mPrimaryWorkTabsView.getPaddingTop();
             int topOffsetAfterAllViewsCollapsed =
-                    topContainerHeight + tabsViewActualHeight - mMaxCollapsibleHeight;
+                    topContainerHeight + mTabsHeight - mCollapsibleHeightForTabs;
 
-            RelativeLayout.LayoutParams layoutParams =
+            RelativeLayout.LayoutParams viewPagerLayoutParams =
                     (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams();
-            layoutParams.setMargins(0, topOffsetAfterAllViewsCollapsed, 0, 0);
-            mPrimaryWorkViewPager.setLayoutParams(layoutParams);
-            mPrimaryWorkViewPager.requestLayout();
+            if (viewPagerLayoutParams.topMargin != topOffsetAfterAllViewsCollapsed) {
+                viewPagerLayoutParams.topMargin = topOffsetAfterAllViewsCollapsed;
+                mPrimaryWorkViewPager.setLayoutParams(viewPagerLayoutParams);
+                hasMarginOrPaddingUpdated = true;
+            }
 
-            mPrimaryRecyclerView.setPadding(
-                    mPrimaryRecyclerView.getPaddingLeft(),
-                    mMaxCollapsibleHeight,
-                    mPrimaryRecyclerView.getPaddingRight(),
-                    mPrimaryRecyclerView.getPaddingBottom());
-            mWorkRecyclerView.setPadding(
-                    mWorkRecyclerView.getPaddingLeft(),
-                    mMaxCollapsibleHeight,
-                    mWorkRecyclerView.getPaddingRight(),
-                    mWorkRecyclerView.getPaddingBottom());
+            if (mPrimaryRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) {
+                mPrimaryRecyclerView.setPadding(
+                        mPrimaryRecyclerView.getPaddingLeft(),
+                        mCollapsibleHeightForTabs,
+                        mPrimaryRecyclerView.getPaddingRight(),
+                        mPrimaryRecyclerView.getPaddingBottom());
+                hasMarginOrPaddingUpdated = true;
+            }
+            if (mWorkRecyclerView.getPaddingTop() != mCollapsibleHeightForTabs) {
+                mWorkRecyclerView.setPadding(
+                        mWorkRecyclerView.getPaddingLeft(),
+                        mCollapsibleHeightForTabs,
+                        mWorkRecyclerView.getPaddingRight(),
+                        mWorkRecyclerView.getPaddingBottom());
+                hasMarginOrPaddingUpdated = true;
+            }
         } else {
-            mPrimaryRecyclerView.setPadding(
-                    mPrimaryRecyclerView.getPaddingLeft(),
-                    topContainerHeight,
-                    mPrimaryRecyclerView.getPaddingRight(),
-                    mPrimaryRecyclerView.getPaddingBottom());
+            if (mPrimaryRecyclerView.getPaddingTop() != topContainerHeight) {
+                mPrimaryRecyclerView.setPadding(
+                        mPrimaryRecyclerView.getPaddingLeft(),
+                        topContainerHeight,
+                        mPrimaryRecyclerView.getPaddingRight(),
+                        mPrimaryRecyclerView.getPaddingBottom());
+                hasMarginOrPaddingUpdated = true;
+            }
         }
-        mSearchRecyclerView.setPadding(
-                mSearchRecyclerView.getPaddingLeft(),
-                topContainerHeight,
-                mSearchRecyclerView.getPaddingRight(),
-                mSearchRecyclerView.getPaddingBottom());
+        if (mSearchRecyclerView.getPaddingTop() != topContainerHeight) {
+            mSearchRecyclerView.setPadding(
+                    mSearchRecyclerView.getPaddingLeft(),
+                    topContainerHeight,
+                    mSearchRecyclerView.getPaddingRight(),
+                    mSearchRecyclerView.getPaddingBottom());
+            hasMarginOrPaddingUpdated = true;
+        }
+        return hasMarginOrPaddingUpdated;
     }
 
     /**
@@ -168,13 +217,22 @@
         // Always use the recycler view offset because fast scroller offset has a different scale.
         int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY();
         if (recyclerViewYOffset < 0) return;
-        if (mMaxCollapsibleHeight > 0) {
-            int yDisplacement = Math.max(-recyclerViewYOffset, -mMaxCollapsibleHeight);
+
+        if (mCollapsibleHeightForRecommendation > 0) {
+            int yDisplacement = Math.max(-recyclerViewYOffset,
+                    -mCollapsibleHeightForRecommendation);
             mViewHolder.mHeaderTitle.setTranslationY(yDisplacement);
-            mViewHolder.mSearchBar.setTranslationY(yDisplacement);
-            if (mHasWorkProfile) {
-                mPrimaryWorkTabsView.setTranslationY(yDisplacement);
-            }
+            mViewHolder.mRecommendedWidgetsTable.setTranslationY(yDisplacement);
+        }
+
+        if (mCollapsibleHeightForSearch > 0) {
+            int searchYDisplacement = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForSearch);
+            mViewHolder.mSearchBar.setTranslationY(searchYDisplacement);
+        }
+
+        if (mHasWorkProfile && mCollapsibleHeightForTabs > 0) {
+            int yDisplacementForTabs = Math.max(-recyclerViewYOffset, -mCollapsibleHeightForTabs);
+            mPrimaryWorkTabsView.setTranslationY(yDisplacementForTabs);
         }
     }
 
@@ -189,6 +247,9 @@
 
     /** private the height, in pixel, + the vertical margins of a given view. */
     private static int measureHeightWithVerticalMargins(View view) {
+        if (view.getVisibility() != View.VISIBLE) {
+            return 0;
+        }
         MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
         return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
                 + marginLayoutParams.topMargin;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index dc375ca..1c34ece 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -33,6 +33,7 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.TextView;
@@ -48,6 +49,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 import com.android.launcher3.widget.BaseWidgetSheet;
@@ -55,6 +57,7 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.search.SearchModeListener;
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
@@ -68,10 +71,15 @@
 public class WidgetsFullSheet extends BaseWidgetSheet
         implements Insettable, ProviderChangedListener, OnActivePageChangedListener,
         WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
+    private static final String TAG = WidgetsFullSheet.class.getSimpleName();
 
     private static final long DEFAULT_OPEN_DURATION = 267;
     private static final long FADE_IN_DURATION = 150;
     private static final float VERTICAL_START_POSITION = 0.3f;
+    // The widget recommendation table can easily take over the entire screen on devices with small
+    // resolution or landscape on phone. This ratio defines the max percentage of content area that
+    // the table can display.
+    private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
 
     private final Rect mInsets = new Rect();
     private final boolean mHasWorkProfile;
@@ -81,10 +89,12 @@
             mCurrentUser.equals(entry.mPkgItem.user);
     private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
             mPrimaryWidgetsFilter.negate();
+    private final int mTabsHeight;
+    private final int mWidgetCellHorizontalPadding;
 
     @Nullable private PersonalWorkPagedView mViewPager;
-    private int mInitialTabsHeight = 0;
     private boolean mIsInSearchMode;
+    private int mMaxSpansPerRow = 4;
     private View mTabsView;
     private TextView mNoWidgetsView;
     private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
@@ -96,6 +106,12 @@
         mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
         mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
         mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
+        mTabsHeight = mHasWorkProfile
+                ? getContext().getResources()
+                        .getDimensionPixelSize(R.dimen.all_apps_header_tab_height)
+                : 0;
+        mWidgetCellHorizontalPadding = 2 * getResources().getDimensionPixelOffset(
+                R.dimen.widget_cell_horizontal_padding);
     }
 
     public WidgetsFullSheet(Context context, AttributeSet attrs) {
@@ -140,6 +156,7 @@
                 findViewById(R.id.search_and_recommendations_container));
         mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController(
                 mHasWorkProfile,
+                mTabsHeight,
                 mSearchAndRecommendationViewHolder,
                 findViewById(R.id.primary_widgets_list_view),
                 mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null,
@@ -150,6 +167,7 @@
 
         mNoWidgetsView = findViewById(R.id.no_widgets_text);
 
+        onRecommendedWidgetsBound();
         onWidgetsBound();
 
         mSearchAndRecommendationViewHolder.mSearchBar.initialize(
@@ -257,6 +275,22 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        doMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) {
+            doMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+
+        if (updateMaxSpansPerRow()) {
+            doMeasure(widthMeasureSpec, heightMeasureSpec);
+
+            if (mSearchAndRecommendationsScrollController.updateMarginAndPadding()) {
+                doMeasure(widthMeasureSpec, heightMeasureSpec);
+            }
+        }
+    }
+
+    private void doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
         int widthUsed;
         if (mInsets.bottom > 0) {
@@ -272,24 +306,29 @@
                 widthUsed, heightMeasureSpec, heightUsed);
         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
                 MeasureSpec.getSize(heightMeasureSpec));
+    }
 
-        int paddingPx = 2 * getResources().getDimensionPixelOffset(
-                R.dimen.widget_cell_horizontal_padding);
-        int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx + paddingPx);
-        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
-                maxSpansPerRow);
-        mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
-                maxSpansPerRow);
-        if (mHasWorkProfile) {
-            mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
-                    maxSpansPerRow);
+    /** Returns {@code true} if the max spans have been updated. */
+    private boolean updateMaxSpansPerRow() {
+        if (getMeasuredWidth() == 0) return false;
+
+        int previousMaxSpansPerRow = mMaxSpansPerRow;
+        mMaxSpansPerRow = getMeasuredWidth()
+                / (mLauncher.getDeviceProfile().cellWidthPx + mWidgetCellHorizontalPadding);
+
+        if (previousMaxSpansPerRow != mMaxSpansPerRow) {
+            mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+                    mMaxSpansPerRow);
+            mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+                    mMaxSpansPerRow);
+            if (mHasWorkProfile) {
+                mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+                        mMaxSpansPerRow);
+            }
+            onRecommendedWidgetsBound();
+            return true;
         }
-
-        if (mInitialTabsHeight == 0 && mTabsView != null) {
-            mInitialTabsHeight = measureHeightWithVerticalMargins(mTabsView);
-        }
-
-        mSearchAndRecommendationsScrollController.updateMarginAndPadding();
+        return false;
     }
 
     @Override
@@ -346,6 +385,8 @@
             mViewPager.snapToPage(AdapterHolder.PRIMARY);
         }
         attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
+
+        mSearchAndRecommendationsScrollController.updateMarginAndPadding();
     }
 
     @Override
@@ -357,6 +398,8 @@
 
     private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
         mIsInSearchMode = isInSearchMode;
+        mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable
+                .setVisibility(isInSearchMode ? GONE : VISIBLE);
         if (mHasWorkProfile) {
             mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE);
             mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE);
@@ -374,6 +417,25 @@
         mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
     }
 
+    @Override
+    public void onRecommendedWidgetsBound() {
+        List<WidgetItem> recommendedWidgets =
+                mLauncher.getPopupDataProvider().getRecommendedWidgets();
+        WidgetsRecommendationTableLayout table =
+                mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable;
+        if (recommendedWidgets.size() > 0) {
+            float maxTableHeight =
+                    (mLauncher.getDeviceProfile().heightPx - mTabsHeight - getHeaderViewHeight())
+                            * RECOMMENDATION_TABLE_HEIGHT_RATIO;
+            List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
+                    WidgetsTableUtils.groupWidgetItemsIntoTable(recommendedWidgets,
+                            mMaxSpansPerRow);
+            table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight);
+        } else {
+            table.setVisibility(GONE);
+        }
+    }
+
     private void open(boolean animate) {
         if (animate) {
             if (getPopupContainer().getInsets().bottom > 0) {
@@ -534,20 +596,29 @@
             mWidgetsRecyclerView.setEdgeEffectFactory(
                     ((TopRoundedCornerView) mContent).createEdgeEffectFactory());
             mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
+            mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow);
         }
     }
 
     final class SearchAndRecommendationViewHolder {
-        final View mContainer;
+        final ViewGroup mContainer;
         final View mCollapseHandle;
         final WidgetsSearchBar mSearchBar;
         final TextView mHeaderTitle;
+        final WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
 
-        SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) {
+        SearchAndRecommendationViewHolder(ViewGroup searchAndRecommendationContainer) {
             mContainer = searchAndRecommendationContainer;
             mCollapseHandle = mContainer.findViewById(R.id.collapse_handle);
             mSearchBar = mContainer.findViewById(R.id.widgets_search_bar);
             mHeaderTitle = mContainer.findViewById(R.id.title);
+            mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table);
+            mRecommendedWidgetsTable.setWidgetCellOnTouchListener((view, event) -> {
+                getRecyclerView().onTouchEvent(event);
+                return false;
+            });
+            mRecommendedWidgetsTable.setWidgetCellLongClickListener(WidgetsFullSheet.this);
+            mRecommendedWidgetsTable.setWidgetCellOnClickListener(WidgetsFullSheet.this);
         }
     }
 }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
new file mode 100644
index 0000000..6569fb0
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -0,0 +1,175 @@
+/*
+ * 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.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A {@link TableLayout} for showing recommended widgets. */
+public final class WidgetsRecommendationTableLayout extends TableLayout {
+    private static final float SCALE_DOWN_RATIO = 0.9f;
+    private final DeviceProfile mDeviceProfile;
+    private final float mWidgetCellTextViewsHeight;
+
+    private float mRecommendationTableMaxHeight = Float.MAX_VALUE;
+    @Nullable private OnLongClickListener mWidgetCellOnLongClickListener;
+    @Nullable private OnClickListener mWidgetCellOnClickListener;
+    @Nullable private OnTouchListener mWidgetCellOnTouchListener;
+
+    public WidgetsRecommendationTableLayout(Context context) {
+        this(context, /* attrs= */ null);
+    }
+
+    public WidgetsRecommendationTableLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
+        // There are 1 row for title, 1 row for dimension and 2 rows for description.
+        mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size);
+    }
+
+    /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */
+    public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) {
+        mWidgetCellOnLongClickListener = onLongClickListener;
+    }
+
+    /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */
+    public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) {
+        mWidgetCellOnClickListener = widgetCellOnClickListener;
+    }
+
+    /** Sets a {@link android.view.View.OnTouchListener} for all widget cells in this table. */
+    public void setWidgetCellOnTouchListener(OnTouchListener widgetCellOnTouchListener) {
+        mWidgetCellOnTouchListener = widgetCellOnTouchListener;
+    }
+
+    /**
+     * Sets a list of recommended widgets that would like to be displayed in this table within the
+     * desired {@code recommendationTableMaxHeight}.
+     *
+     * <p>If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a
+     * last row from the {@code recommendedWidgets} until it fits or only one row left. If the only
+     * row still doesn't fit, we scale down the preview image.
+     */
+    public void setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
+            float recommendationTableMaxHeight) {
+        mRecommendationTableMaxHeight = recommendationTableMaxHeight;
+        RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
+                recommendedWidgets);
+        bindData(data);
+    }
+
+    private void bindData(RecommendationTableData data) {
+        if (data.mRecommendationTable.size() == 0) {
+            setVisibility(GONE);
+            return;
+        }
+
+        removeAllViews();
+
+        for (int i = 0; i < data.mRecommendationTable.size(); i++) {
+            List<WidgetItem> widgetItems = data.mRecommendationTable.get(i);
+            TableRow tableRow = new TableRow(getContext());
+            tableRow.setGravity(Gravity.TOP);
+
+            for (WidgetItem widgetItem : widgetItems) {
+                WidgetCell widgetCell = addItemCell(tableRow);
+                widgetCell.setPreviewSize(widgetItem.spanX, widgetItem.spanY, data.mPreviewScale);
+                widgetCell.applyFromCellItem(widgetItem,
+                        LauncherAppState.getInstance(getContext()).getWidgetCache());
+                widgetCell.ensurePreview();
+            }
+            addView(tableRow);
+        }
+        setVisibility(VISIBLE);
+    }
+
+    private WidgetCell addItemCell(ViewGroup parent) {
+        WidgetCell widget = (WidgetCell) LayoutInflater.from(
+                getContext()).inflate(R.layout.widget_cell, parent, false);
+
+        widget.setOnTouchListener(mWidgetCellOnTouchListener);
+        WidgetImageView preview = widget.findViewById(R.id.widget_preview);
+        preview.setOnClickListener(mWidgetCellOnClickListener);
+        preview.setOnLongClickListener(mWidgetCellOnLongClickListener);
+        widget.setAnimatePreview(false);
+
+        parent.addView(widget);
+        return widget;
+    }
+
+    private RecommendationTableData fitRecommendedWidgetsToTableSpace(
+            float previewScale,
+            List<ArrayList<WidgetItem>> recommendedWidgetsInTable) {
+        // A naive estimation of the widgets recommendation table height without inflation.
+        float totalHeight = 0;
+        for (int i = 0; i < recommendedWidgetsInTable.size(); i++) {
+            List<WidgetItem> widgetItems = recommendedWidgetsInTable.get(i);
+            float rowHeight = 0;
+            for (int j = 0; j < widgetItems.size(); j++) {
+                float previewHeight = widgetItems.get(j).spanY * mDeviceProfile.allAppsCellHeightPx
+                        * previewScale;
+                rowHeight = Math.max(rowHeight, previewHeight + mWidgetCellTextViewsHeight);
+            }
+            totalHeight += rowHeight;
+        }
+
+        if (totalHeight < mRecommendationTableMaxHeight) {
+            return new RecommendationTableData(recommendedWidgetsInTable, previewScale);
+        }
+
+        if (recommendedWidgetsInTable.size() > 1) {
+            // We don't want to scale down widgets preview unless we really need to. Reduce the
+            // num of row by 1 to see if it fits.
+            return fitRecommendedWidgetsToTableSpace(
+                    previewScale,
+                    recommendedWidgetsInTable.subList(/* fromIndex= */0,
+                            /* toIndex= */recommendedWidgetsInTable.size() - 1));
+        }
+
+        float nextPreviewScale = previewScale * SCALE_DOWN_RATIO;
+        return fitRecommendedWidgetsToTableSpace(nextPreviewScale, recommendedWidgetsInTable);
+    }
+
+    /** Data class for the widgets recommendation table and widgets preview scaling. */
+    private class RecommendationTableData {
+        private final List<ArrayList<WidgetItem>> mRecommendationTable;
+        private final float mPreviewScale;
+
+        RecommendationTableData(List<ArrayList<WidgetItem>> recommendationTable,
+                float previewScale) {
+            mRecommendationTable = recommendationTable;
+            mPreviewScale = previewScale;
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 22f4d31..fe4c712 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -30,7 +30,6 @@
 import com.android.launcher3.testing.TestProtocol;
 
 import java.util.Collection;
-import java.util.List;
 
 /**
  * All widgets container.
@@ -106,7 +105,8 @@
                     fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
             final Point displaySize = mLauncher.getRealDisplaySize();
 
-            final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer();
+            Rect headerRect = new Rect();
+            final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer(headerRect);
             mLauncher.assertTrue("Can't locate widgets list for the test app: "
                             + mLauncher.getLauncherPackageName(),
                     widgetsContainer != null);
@@ -132,7 +132,12 @@
 
                 mLauncher.assertTrue("Too many attempts", ++i <= 40);
                 final int scroll = getWidgetsScroll();
-                mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, tableRows, 0);
+                mLauncher.scroll(
+                        fullWidgetsPicker,
+                        Direction.DOWN,
+                        headerRect,
+                        10,
+                        true);
                 final int newScroll = getWidgetsScroll();
                 mLauncher.assertTrue(
                         "Scrolled in a wrong direction in Widgets: from " + scroll + " to "
@@ -143,7 +148,7 @@
     }
 
     /** Finds the widgets list of this test app from the collapsed full widgets picker. */
-    private UiObject2 findTestAppWidgetsTableContainer() {
+    private UiObject2 findTestAppWidgetsTableContainer(Rect outHeaderRect) {
         final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(),
                 "widgets_list_header");
         final BySelector targetAppSelector = By.clazz("android.widget.TextView").text(
@@ -156,6 +161,7 @@
             UiObject2 fullWidgetsPicker = verifyActiveContainer();
 
             UiObject2 header = fullWidgetsPicker.findObject(headerSelector);
+            outHeaderRect.set(0, 0, 0, header.getVisibleBounds().height());
             mLauncher.assertTrue("Can't find a widget header", header != null);
 
             // Look for a header that has the test app name.
@@ -179,11 +185,14 @@
                 if (widgetsContainer != null) {
                     return widgetsContainer;
                 }
-                mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, List.of(headerTitle), 0);
-            } else {
-                mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, fullWidgetsPicker.getChildren(),
-                        0);
+
             }
+            mLauncher.scroll(
+                    fullWidgetsPicker,
+                    Direction.DOWN,
+                    outHeaderRect,
+                    /* steps= */ 10,
+                    /* slowDown= */ true);
         }
 
         return null;