Initial UI changes for displaying categorized suggestions.

Ordering changes and some more UI changes in follow-up.

Bug: 318410881
Test: See screenshots
Flag: ACONFIG com.android.launcher3.enable_categorized_widget_recommendations DEVELOPMENT
Change-Id: I77e7f4dcdda32e2921ae56721cddbe261832f0d8
diff --git a/res/layout/widget_recommendations.xml b/res/layout/widget_recommendations.xml
new file mode 100644
index 0000000..89821ac
--- /dev/null
+++ b/res/layout/widget_recommendations.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 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.
+  -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+    <!--
+    Shown when there are more than one pages
+    Note: on page change, using accessibility live region lets user know that the title has changed.
+    -->
+    <TextView
+        android:id="@+id/recommendations_page_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="12dp"
+        android:layout_marginTop="16dp"
+        android:accessibilityLiveRegion="polite"
+        android:gravity="center_horizontal"
+        android:lineHeight="20sp"
+        android:textColor="?attr/widgetPickerTitleColor"
+        android:textFontWeight="500"
+        android:textSize="16sp"
+        android:visibility="gone" />
+    <!-- Shown when there are more than one pages -->
+    <com.android.launcher3.pageindicators.PageIndicatorDots
+        android:id="@+id/widget_recommendations_page_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:elevation="1dp"
+        android:visibility="gone" />
+    <!--
+     Note: importantForAccessibility = yes on this view ensures that with talkback, when user
+     swipes right on the last item in current page, they are taken to the next page. And, doing
+     the same on the last page, takes them to the next section e.g. apps list in single pane
+     picker.
+    -->
+    <com.android.launcher3.widget.picker.WidgetRecommendationsView
+        android:id="@+id/widget_recommendations_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:background="@drawable/widgets_surface_background"
+        android:importantForAccessibility="yes"
+        launcher:pageIndicator="@+id/widget_recommendations_page_indicator" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/widget_recommendations_table.xml b/res/layout/widget_recommendations_table.xml
new file mode 100644
index 0000000..e3f0562
--- /dev/null
+++ b/res/layout/widget_recommendations_table.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 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.
+  -->
+<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/widget_recommendations_table_horizontal_padding"
+    android:paddingVertical="@dimen/widget_recommendations_table_vertical_padding" />
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index 069d4bc..1d37043 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -73,15 +73,18 @@
             <include layout="@layout/widgets_search_bar" />
         </FrameLayout>
 
-        <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-            android:id="@+id/recommended_widget_table"
+        <!-- Shown when there are recommendations to display -->
+        <LinearLayout
+            android:id="@+id/widget_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginTop="8dp"
-            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
             android:background="@drawable/widgets_surface_background"
-            android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
-            android:visibility="gone" />
+            android:orientation="vertical"
+            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
+            android:visibility="gone">
+            <include layout="@layout/widget_recommendations" />
+        </LinearLayout>
 
         <com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
             android:id="@+id/tabs"
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 25bbad4..dca08ff 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -56,14 +56,17 @@
             <include layout="@layout/widgets_search_bar" />
         </FrameLayout>
 
-        <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-            android:id="@+id/recommended_widget_table"
+        <!-- Shown when there are recommendations to display -->
+        <LinearLayout
+            android:id="@+id/widget_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginTop="8dp"
             android:background="@drawable/widgets_surface_background"
-            android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
-            android:visibility="gone" />
+            android:orientation="vertical"
+            android:visibility="gone">
+            <include layout="@layout/widget_recommendations" />
+        </LinearLayout>
     </com.android.launcher3.views.StickyHeaderLayout>
 
 </merge>
\ No newline at end of file
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index f692e24..8e45740f 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -118,13 +118,16 @@
                         android:background="@drawable/widgets_surface_background"
                         android:importantForAccessibility="yes"
                         android:id="@+id/right_pane">
-                        <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-                            android:id="@+id/recommended_widget_table"
+                        <!-- Shown when there are recommendations to display -->
+                        <LinearLayout
+                            android:id="@+id/widget_recommendations_container"
                             android:layout_width="match_parent"
                             android:layout_height="wrap_content"
-                            android:paddingHorizontal=
-                                "@dimen/widget_list_horizontal_margin_two_pane"
-                            android:visibility="gone" />
+                            android:background="@drawable/widgets_surface_background"
+                            android:orientation="vertical"
+                            android:visibility="gone">
+                            <include layout="@layout/widget_recommendations" />
+                        </LinearLayout>
                     </LinearLayout>
                 </ScrollView>
             </FrameLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 6189961..2f0f130 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -182,9 +182,8 @@
     <dimen name="widget_apps_tabs_vertical_padding">6dp</dimen>
     <dimen name="widget_picker_landscape_tablet_left_right_margin">117dp</dimen>
     <dimen name="widget_picker_two_panels_left_right_margin">0dp</dimen>
-
-    <dimen name="recommended_widgets_table_vertical_padding">8dp</dimen>
-
+    <dimen name="widget_recommendations_table_vertical_padding">8dp</dimen>
+    <dimen name="widget_recommendations_table_horizontal_padding">16dp</dimen>
     <!-- Bottom margin for the search and recommended widgets container without work profile -->
     <dimen name="search_and_recommended_widgets_container_bottom_margin">16dp</dimen>
     <!-- Bottom margin for the search and recommended widgets container with work profile -->
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a4e7ec4..231aec4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -71,7 +71,7 @@
     <!-- Widget suggestions header title in the full widgets picker for large screen devices
     in landscape mode. [CHAR_LIMIT=50] -->
     <string name="suggested_widgets_header_title">Suggestions</string>
-    <string name="productivity_widget_recommendation_category_label">Boost your day</string>
+    <string name="productivity_widget_recommendation_category_label">Your Daily Essentials</string>
     <string name="news_widget_recommendation_category_label">News For You</string>
     <string name="social_and_entertainment_widget_recommendation_category_label">Your Chill Zone</string>
     <string name="fitness_widget_recommendation_category_label">Reach Your Fitness Goals</string>
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index df369c6..1b5abaa 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -367,7 +367,7 @@
         mNumPages = numMarkers;
 
         // If the last page gets removed we want to go to the previous page.
-        if (mNumPages == mActivePage) {
+        if (mNumPages > 0 && mNumPages == mActivePage) {
             mActivePage--;
             CURRENT_POSITION.set(this, (float) mActivePage);
         }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index f1d837c..fb463f7 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -222,6 +222,7 @@
     }
 
     /** Returns the recommended widgets mapped by their category. */
+    @NonNull
     public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
         Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
                 .filter(entry -> entry instanceof WidgetsListContentEntry)
@@ -232,7 +233,8 @@
                         Function.identity()
                 ));
         return mRecommendedWidgets.stream()
-                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo)
+                .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
+                        && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
                 .collect(Collectors.groupingBy(
                         it -> ((PendingAddWidgetInfo) it).recommendationCategory,
                         Collectors.collectingAndThen(
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index c75f9d1..94c630a 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -367,6 +367,16 @@
         }
     }
 
+    /**
+     * Shows or hides the long description displayed below each widget.
+     *
+     * @param show a flag that shows the long description of the widget if {@code true}, hides it if
+     *             {@code false}.
+     */
+    public void showDescription(boolean show) {
+        mWidgetDescription.setVisibility(show ? VISIBLE : GONE);
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
new file mode 100644
index 0000000..738d74e
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pageindicators.PageIndicatorDots;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A {@link PagedView} that displays widget recommendations in categories with dots as paged
+ * indicators.
+ */
+public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
+    private @Px float mAvailableHeight = Float.MAX_VALUE;
+
+    private static final int MAX_CATEGORIES = 3;
+    private TextView mRecommendationPageTitle;
+    private final List<String> mCategoryTitles = new ArrayList<>();
+
+    @Nullable
+    private OnLongClickListener mWidgetCellOnLongClickListener;
+    @Nullable
+    private OnClickListener mWidgetCellOnClickListener;
+
+    public WidgetRecommendationsView(Context context) {
+        this(context, /* attrs= */ null);
+    }
+
+    public WidgetRecommendationsView(Context context, AttributeSet attrs) {
+        this(context, attrs, /* defStyleAttr= */ 0);
+    }
+
+    public WidgetRecommendationsView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void initParentViews(View parent) {
+        super.initParentViews(parent);
+        mRecommendationPageTitle = parent.findViewById(R.id.recommendations_page_title);
+    }
+
+    /** 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;
+    }
+
+    /**
+     * Displays all the provided recommendations in a single table if they fit.
+     *
+     * @param recommendedWidgets list of widgets to be displayed in recommendation section.
+     * @param availableHeight    height in px that can be used to display the recommendations;
+     *                           recommendations that don't fit in this height won't be shown
+     * @param availableWidth     width in px that the recommendations should display in
+     * @param cellPadding        padding in px that should be applied to each widget in the
+     *                           recommendations
+     * @return {@code false} if no recommendations could fit in the available space.
+     */
+    public boolean setRecommendations(
+            List<WidgetItem> recommendedWidgets, final @Px float availableHeight,
+            final @Px int availableWidth, final @Px int cellPadding) {
+        this.mAvailableHeight = availableHeight;
+        removeAllViews();
+
+        maybeDisplayInTable(recommendedWidgets, availableWidth, cellPadding);
+        updateTitleAndIndicator();
+        return getChildCount() > 0;
+    }
+
+    /**
+     * Displays the recommendations grouped by categories as pages.
+     * <p>In case of a single category, no title is displayed for it.</p>
+     *
+     * @param recommendations a map of widget items per recommendation category
+     * @param availableHeight height in px that can be used to display the recommendations;
+     *                        recommendations that don't fit in this height won't be shown
+     * @param availableWidth  width in px that the recommendations should display in
+     * @param cellPadding     padding in px that should be applied to each widget in the
+     *                        recommendations
+     * @return {@code false} if no recommendations could fit in the available space.
+     */
+    public boolean setRecommendations(
+            Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
+            final @Px float availableHeight, final @Px int availableWidth,
+            final @Px int cellPadding) {
+        this.mAvailableHeight = availableHeight;
+        Context context = getContext();
+        removeAllViews();
+
+        int displayedCategories = 0;
+
+        // Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
+        for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
+                new TreeMap<>(recommendations).entrySet()) {
+            // If none of the recommendations for the category could fit in the mAvailableHeight, we
+            // don't want to add that category; and we look for the next one.
+            if (maybeDisplayInTable(entry.getValue(), availableWidth, cellPadding)) {
+                mCategoryTitles.add(
+                        context.getResources().getString(entry.getKey().categoryTitleRes));
+                displayedCategories++;
+            }
+
+            if (displayedCategories == MAX_CATEGORIES) {
+                break;
+            }
+        }
+
+        updateTitleAndIndicator();
+        return getChildCount() > 0;
+    }
+
+    /** Displays the page title and paging indicator if there are multiple pages. */
+    private void updateTitleAndIndicator() {
+        int titleAndIndicatorVisibility = getPageCount() > 1 ? View.VISIBLE : View.GONE;
+        mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility);
+        mPageIndicator.setVisibility(titleAndIndicatorVisibility);
+    }
+
+    @Override
+    protected void notifyPageSwitchListener(int prevPage) {
+        if (getPageCount() > 1) {
+            // Since the title is outside the paging scroll, we update the title on page switch.
+            mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage()));
+            super.notifyPageSwitchListener(prevPage);
+            requestLayout();
+        }
+    }
+
+    @Override
+    protected boolean canScroll(float absVScroll, float absHScroll) {
+        // Allow only horizontal scroll.
+        return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        mPageIndicator.setScroll(l, mMaxScroll);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        boolean hasMultiplePages = getChildCount() > 0;
+
+        if (hasMultiplePages) {
+            int finalWidth = MeasureSpec.getSize(widthMeasureSpec);
+            int desiredHeight = 0;
+
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                measureChild(child, widthMeasureSpec, heightMeasureSpec);
+                if (mAvailableHeight == Float.MAX_VALUE) {
+                    // When we are not limited by height, use currentPage's height. This is the case
+                    // when the paged layout is placed in a scrollable container. We cannot use
+                    // height
+                    // of tallest child in such case, as it will display a scrollbar even for
+                    // smaller
+                    // pages that don't have more content.
+                    if (i == mCurrentPage) {
+                        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+                        desiredHeight = Math.max(parentHeight, child.getMeasuredHeight());
+                    }
+                } else {
+                    // Use height of tallest child when we are limited to a certain height.
+                    desiredHeight = Math.max(desiredHeight, child.getMeasuredHeight());
+                }
+            }
+
+            int finalHeight = resolveSizeAndState(desiredHeight, heightMeasureSpec, 0);
+            setMeasuredDimension(finalWidth, finalHeight);
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    /**
+     * Groups the provided recommendations into rows and displays them in a table if at least one
+     * fits.
+     * <p>Returns false if none of the recommendations could fit.</p>
+     */
+    private boolean maybeDisplayInTable(List<WidgetItem> recommendedWidgets,
+            final @Px int availableWidth, final @Px int cellPadding) {
+        Context context = getContext();
+        DeviceProfile deviceProfile = Launcher.getLauncher(context).getDeviceProfile();
+        LayoutInflater inflater = LayoutInflater.from(context);
+
+        List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
+                recommendedWidgets,
+                context,
+                deviceProfile,
+                availableWidth,
+                cellPadding);
+
+        WidgetsRecommendationTableLayout recommendationsTable =
+                (WidgetsRecommendationTableLayout) inflater.inflate(
+                        R.layout.widget_recommendations_table,
+                        /* root=*/ this,
+                        /* attachToRoot=*/ false);
+        recommendationsTable.setWidgetCellOnClickListener(mWidgetCellOnClickListener);
+        recommendationsTable.setWidgetCellLongClickListener(mWidgetCellOnLongClickListener);
+
+        boolean displayedAtLeastOne = recommendationsTable.setRecommendedWidgets(rows,
+                mAvailableHeight);
+        if (displayedAtLeastOne) {
+            addView(recommendationsTable);
+        }
+
+        return displayedAtLeastOne;
+    }
+
+    /** Returns location of a widget cell for displaying the "touch and hold" education tip. */
+    public View getViewForEducationTip() {
+        if (getChildCount() > 0) {
+            // first page (a table layout) -> first item (a widget cell).
+            return ((ViewGroup) getChildAt(0)).getChildAt(0);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index e321d83..237078e 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -46,6 +46,7 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.Button;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.window.BackEvent;
 
@@ -66,7 +67,6 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.model.UserManagerState;
-import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.views.ArrowTipView;
 import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -77,7 +77,6 @@
 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;
 
@@ -168,7 +167,8 @@
 
     protected TextView mNoWidgetsView;
     protected StickyHeaderLayout mSearchScrollView;
-    protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
+    protected WidgetRecommendationsView mWidgetRecommendationsView;
+    protected LinearLayout mWidgetRecommendationsContainer;
     protected View mTabBar;
     protected View mSearchBarContainer;
     protected WidgetsSearchBar mSearchBar;
@@ -222,9 +222,14 @@
 
         setupViews();
 
-        mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
-        mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
-        mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
+        mWidgetRecommendationsContainer = mSearchScrollView.findViewById(
+                R.id.widget_recommendations_container);
+        mWidgetRecommendationsView = mSearchScrollView.findViewById(
+                R.id.widget_recommendations_view);
+        mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
+        mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
+        mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+
         mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
 
         onRecommendedWidgetsBound();
@@ -552,7 +557,7 @@
     protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
         mIsInSearchMode = isInSearchMode;
         if (isInSearchMode) {
-            mRecommendedWidgetsTable.setVisibility(GONE);
+            mWidgetRecommendationsContainer.setVisibility(GONE);
             if (mHasWorkProfile) {
                 mViewPager.setVisibility(GONE);
                 mTabBar.setVisibility(GONE);
@@ -581,40 +586,44 @@
         if (mIsInSearchMode) {
             return;
         }
-        List<WidgetItem> recommendedWidgets =
-                mActivityContext.getPopupDataProvider().getRecommendedWidgets();
-        mHasRecommendedWidgets = recommendedWidgets.size() > 0;
-        if (mHasRecommendedWidgets) {
-            float noWidgetsViewHeight = 0;
-            if (mIsNoWidgetsViewNeeded) {
-                // Make sure recommended section leaves enough space for noWidgetsView.
-                Rect noWidgetsViewTextBounds = new Rect();
-                mNoWidgetsView.getPaint()
-                        .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
-                                mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
-                noWidgetsViewHeight = noWidgetsViewTextBounds.height();
-            }
-            if (!isTwoPane()) {
-                doMeasure(
-                        makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
-                                MeasureSpec.EXACTLY),
-                        makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
-                                MeasureSpec.EXACTLY));
-            }
-            float maxTableHeight = getMaxTableHeight(noWidgetsViewHeight);
 
-            List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
-                    WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering(
-                            recommendedWidgets,
-                            mActivityContext,
-                            mActivityContext.getDeviceProfile(),
-                            mMaxSpanPerRow,
-                            mWidgetCellHorizontalPadding);
-            mRecommendedWidgetsTable.setRecommendedWidgets(
-                    recommendedWidgetsInTable, maxTableHeight);
+        if (enableCategorizedWidgetSuggestions()) {
+            mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
+                    mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
+                    /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+                    /* availableWidth= */ mMaxSpanPerRow,
+                    /* cellPadding= */ mWidgetCellHorizontalPadding
+            );
         } else {
-            mRecommendedWidgetsTable.setVisibility(GONE);
+            mHasRecommendedWidgets = mWidgetRecommendationsView.setRecommendations(
+                    mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+                    /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+                    /* availableWidth= */ mMaxSpanPerRow,
+                    /* cellPadding= */ mWidgetCellHorizontalPadding
+            );
         }
+        mWidgetRecommendationsContainer.setVisibility(mHasRecommendedWidgets ? VISIBLE : GONE);
+    }
+
+    @Px
+    private float getMaxAvailableHeightForRecommendations() {
+        float noWidgetsViewHeight = 0;
+        if (mIsNoWidgetsViewNeeded) {
+            // Make sure recommended section leaves enough space for noWidgetsView.
+            Rect noWidgetsViewTextBounds = new Rect();
+            mNoWidgetsView.getPaint()
+                    .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
+                            mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
+            noWidgetsViewHeight = noWidgetsViewTextBounds.height();
+        }
+        if (!isTwoPane()) {
+            doMeasure(
+                    makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
+                            MeasureSpec.EXACTLY),
+                    makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
+                            MeasureSpec.EXACTLY));
+        }
+        return getMaxTableHeight(noWidgetsViewHeight);
     }
 
     /** b/209579563: "Widgets" header should be focused first. */
@@ -623,7 +632,8 @@
         return mHeaderTitle;
     }
 
-    protected float getMaxTableHeight(float noWidgetsViewHeight) {
+    @Px
+    protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
         return (mContent.getMeasuredHeight()
                 - mTabsHeight - getHeaderViewHeight()
                 - noWidgetsViewHeight)
@@ -870,9 +880,8 @@
     }
 
     @Nullable private View getViewToShowEducationTip() {
-        if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
-                && mRecommendedWidgetsTable.getChildCount() > 0) {
-            return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
+        if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
+            return mWidgetRecommendationsView.getViewForEducationTip();
         }
 
         AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 2d17033..ce1f4e0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -62,7 +62,7 @@
         super(context, attrs);
         // There are 1 row for title, 1 row for dimension and 2 rows for description.
         mWidgetsRecommendationTableVerticalPadding = 2 * getResources()
-                .getDimensionPixelSize(R.dimen.recommended_widgets_table_vertical_padding);
+                .getDimensionPixelSize(R.dimen.widget_recommendations_table_vertical_padding);
         mWidgetCellVerticalPadding = 2 * getResources()
                 .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding);
         mWidgetCellTextViewsHeight = 4 * getResources().getDimension(R.dimen.widget_cell_font_size);
@@ -85,17 +85,20 @@
      * <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.
+     *
+     * <p>Returns {@code false} if none of the widgets could fit</p>
      */
-    public void setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
+    public boolean setRecommendedWidgets(List<ArrayList<WidgetItem>> recommendedWidgets,
             float recommendationTableMaxHeight) {
         mRecommendationTableMaxHeight = recommendationTableMaxHeight;
         RecommendationTableData data = fitRecommendedWidgetsToTableSpace(/* previewScale= */ 1f,
                 recommendedWidgets);
         bindData(data);
+        return !data.mRecommendationTable.isEmpty();
     }
 
     private void bindData(RecommendationTableData data) {
-        if (data.mRecommendationTable.size() == 0) {
+        if (data.mRecommendationTable.isEmpty()) {
             setVisibility(GONE);
             return;
         }
@@ -116,6 +119,9 @@
                 WidgetCell widgetCell = addItemCell(tableRow);
                 widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale);
                 widgetCell.showBadge();
+                if (enableCategorizedWidgetSuggestions()) {
+                    widgetCell.showDescription(false);
+                }
             }
             addView(tableRow);
         }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 4326515..165b2fe 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -110,9 +110,15 @@
 
         mWidgetsListTableViewHolderBinder =
                 new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this);
-        mRecommendedWidgetsTable = mContent.findViewById(R.id.recommended_widget_table);
-        mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
-        mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
+
+        mWidgetRecommendationsContainer = mContent.findViewById(
+                R.id.widget_recommendations_container);
+        mWidgetRecommendationsView = mContent.findViewById(
+                R.id.widget_recommendations_view);
+        mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
+        mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
+        mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+
         mHeaderTitle = mContent.findViewById(R.id.title);
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPane.setOutlineProvider(mViewOutlineProviderRightPane);
@@ -214,7 +220,7 @@
             mSuggestedWidgetsHeader.setExpanded(true);
             resetExpandedHeaders();
             mRightPane.removeAllViews();
-            mRightPane.addView(mRecommendedWidgetsTable);
+            mRightPane.addView(mWidgetRecommendationsContainer);
             mRightPaneScrollView.setScrollY(0);
             mRightPane.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
             mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
@@ -225,7 +231,8 @@
     }
 
     @Override
-    protected float getMaxTableHeight(float noWidgetsViewHeight) {
+    @Px
+    protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
         return Float.MAX_VALUE;
     }