1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.widget.picker;
17 
18 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
19 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
20 import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx;
21 import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_SIZE_COMPARATOR;
22 
23 import static java.lang.Math.max;
24 
25 import android.content.Context;
26 import android.util.AttributeSet;
27 import android.view.Gravity;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.widget.TableLayout;
31 
32 import androidx.annotation.Nullable;
33 import androidx.annotation.Px;
34 
35 import com.android.launcher3.DeviceProfile;
36 import com.android.launcher3.R;
37 import com.android.launcher3.model.WidgetItem;
38 import com.android.launcher3.widget.WidgetCell;
39 import com.android.launcher3.widget.WidgetTableRow;
40 import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 /** A {@link TableLayout} for showing recommended widgets. */
46 public final class WidgetsRecommendationTableLayout extends TableLayout {
47     private final float mWidgetsRecommendationTableVerticalPadding;
48     private final float mWidgetCellVerticalPadding;
49     private final float mWidgetCellTextViewsHeight;
50 
51     @Nullable private OnLongClickListener mWidgetCellOnLongClickListener;
52     @Nullable private OnClickListener mWidgetCellOnClickListener;
53 
WidgetsRecommendationTableLayout(Context context)54     public WidgetsRecommendationTableLayout(Context context) {
55         this(context, /* attrs= */ null);
56     }
57 
WidgetsRecommendationTableLayout(Context context, AttributeSet attrs)58     public WidgetsRecommendationTableLayout(Context context, AttributeSet attrs) {
59         super(context, attrs);
60         // There are 1 row for title, 1 row for dimension and max 3 rows for description.
61         mWidgetsRecommendationTableVerticalPadding = 2 * getResources()
62                 .getDimensionPixelSize(R.dimen.widget_recommendations_table_vertical_padding);
63         mWidgetCellVerticalPadding = 2 * getResources()
64                 .getDimensionPixelSize(R.dimen.widget_cell_vertical_padding);
65         mWidgetCellTextViewsHeight =
66                 getResources().getDimension(R.dimen.widget_cell_title_line_height);
67     }
68 
69     /** Sets a {@link android.view.View.OnLongClickListener} for all widget cells in this table. */
setWidgetCellLongClickListener(OnLongClickListener onLongClickListener)70     public void setWidgetCellLongClickListener(OnLongClickListener onLongClickListener) {
71         mWidgetCellOnLongClickListener = onLongClickListener;
72     }
73 
74     /** Sets a {@link android.view.View.OnClickListener} for all widget cells in this table. */
setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener)75     public void setWidgetCellOnClickListener(OnClickListener widgetCellOnClickListener) {
76         mWidgetCellOnClickListener = widgetCellOnClickListener;
77     }
78 
79     /**
80      * Sets a list of recommended widgets that would like to be displayed in this table within the
81      * desired {@code recommendationTableMaxHeight}.
82      *
83      * <p>If the content can't fit {@code recommendationTableMaxHeight}, this view will remove a
84      * last row from the {@code recommendedWidgets} until it fits or only one row left.
85      *
86      * <p>Returns the list of widgets that could fit</p>
87      */
setRecommendedWidgets( List<ArrayList<WidgetItem>> recommendedWidgets, DeviceProfile deviceProfile, float recommendationTableMaxHeight)88     public List<ArrayList<WidgetItem>> setRecommendedWidgets(
89             List<ArrayList<WidgetItem>> recommendedWidgets,
90             DeviceProfile deviceProfile, float recommendationTableMaxHeight) {
91         List<ArrayList<WidgetItem>> rows = selectRowsThatFitInAvailableHeight(recommendedWidgets,
92                 recommendationTableMaxHeight, deviceProfile);
93         bindData(rows);
94         return rows;
95     }
96 
bindData(List<ArrayList<WidgetItem>> recommendationTable)97     private void bindData(List<ArrayList<WidgetItem>> recommendationTable) {
98         if (recommendationTable.isEmpty()) {
99             setVisibility(GONE);
100             return;
101         }
102 
103         removeAllViews();
104 
105         for (int i = 0; i < recommendationTable.size(); i++) {
106             List<WidgetItem> widgetItems = recommendationTable.get(i);
107             WidgetTableRow tableRow = new WidgetTableRow(getContext());
108             tableRow.setupRow(widgetItems.size(), /*resizeDelayMs=*/ 0);
109             tableRow.setGravity(Gravity.TOP);
110             for (WidgetItem widgetItem : widgetItems) {
111                 WidgetCell widgetCell = addItemCell(tableRow);
112                 widgetCell.applyFromCellItem(widgetItem);
113                 widgetCell.showAppIconInWidgetTitle(true);
114                 if (enableCategorizedWidgetSuggestions()) {
115                     widgetCell.showDescription(false);
116                     widgetCell.showDimensions(false);
117                 }
118             }
119             addView(tableRow);
120         }
121         setVisibility(VISIBLE);
122     }
123 
addItemCell(WidgetTableRow parent)124     private WidgetCell addItemCell(WidgetTableRow parent) {
125         WidgetCell widget = (WidgetCell) LayoutInflater.from(
126                 getContext()).inflate(R.layout.widget_cell, parent, false);
127         widget.addPreviewReadyListener(parent);
128         widget.setOnClickListener(mWidgetCellOnClickListener);
129 
130         View previewContainer = widget.findViewById(R.id.widget_preview_container);
131         previewContainer.setOnClickListener(mWidgetCellOnClickListener);
132         previewContainer.setOnLongClickListener(mWidgetCellOnLongClickListener);
133         widget.setAnimatePreview(false);
134         widget.setSourceContainer(CONTAINER_WIDGETS_PREDICTION);
135 
136         parent.addView(widget);
137         return widget;
138     }
139 
selectRowsThatFitInAvailableHeight( List<ArrayList<WidgetItem>> recommendedWidgets, @Px float recommendationTableMaxHeight, DeviceProfile deviceProfile)140     private List<ArrayList<WidgetItem>> selectRowsThatFitInAvailableHeight(
141             List<ArrayList<WidgetItem>> recommendedWidgets, @Px float recommendationTableMaxHeight,
142             DeviceProfile deviceProfile) {
143         List<ArrayList<WidgetItem>> filteredRows = new ArrayList<>();
144         // A naive estimation of the widgets recommendation table height without inflation.
145         float totalHeight = mWidgetsRecommendationTableVerticalPadding;
146 
147         for (int i = 0; i < recommendedWidgets.size(); i++) {
148             List<WidgetItem> widgetItems = recommendedWidgets.get(i);
149             float rowHeight = 0;
150             for (int j = 0; j < widgetItems.size(); j++) {
151                 WidgetItem widgetItem = widgetItems.get(j);
152                 WidgetPreviewContainerSize previewContainerSize =
153                         WidgetPreviewContainerSize.Companion.forItem(widgetItem, deviceProfile);
154                 float widgetItemHeight = getWidgetSizePx(deviceProfile, previewContainerSize.spanX,
155                         previewContainerSize.spanY).getHeight();
156                 rowHeight = max(rowHeight,
157                         widgetItemHeight + mWidgetCellTextViewsHeight + mWidgetCellVerticalPadding);
158             }
159             if (totalHeight + rowHeight <= recommendationTableMaxHeight) {
160                 totalHeight += rowHeight;
161                 filteredRows.add(new ArrayList<>(widgetItems));
162             }
163         }
164 
165         // Perform re-ordering once we have filtered out recommendations that fit.
166         return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
167     }
168 }
169