• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.enableTieredWidgetsByDefaultInPicker;
19 import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
20 import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
21 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
22 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
23 import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
24 import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY;
25 import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser;
26 
27 import android.content.Context;
28 import android.graphics.Rect;
29 import android.os.Process;
30 import android.os.UserHandle;
31 import android.util.AttributeSet;
32 import android.view.Gravity;
33 import android.view.LayoutInflater;
34 import android.view.MenuItem;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.ViewParent;
39 import android.view.accessibility.AccessibilityNodeInfo;
40 import android.widget.FrameLayout;
41 import android.widget.LinearLayout;
42 import android.widget.PopupMenu;
43 import android.widget.ScrollView;
44 import android.widget.TextView;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.annotation.Px;
49 
50 import com.android.launcher3.DeviceProfile;
51 import com.android.launcher3.R;
52 import com.android.launcher3.Utilities;
53 import com.android.launcher3.icons.cache.CacheLookupFlag;
54 import com.android.launcher3.model.WidgetItem;
55 import com.android.launcher3.model.data.PackageItemInfo;
56 import com.android.launcher3.recyclerview.ViewHolderBinder;
57 import com.android.launcher3.util.PackageUserKey;
58 import com.android.launcher3.widget.WidgetCell;
59 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
60 import com.android.launcher3.widget.model.WidgetsListContentEntry;
61 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
62 
63 import java.util.Collections;
64 import java.util.List;
65 
66 /**
67  * Popup for showing the full list of available widgets with a two-pane layout.
68  */
69 public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
70     private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268;
71     private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
72     private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
73 
74     // This ratio defines the max percentage of content area that the recommendations can display
75     // with respect to the bottom sheet's height.
76     private static final float RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE = 0.70f;
77     private FrameLayout mSuggestedWidgetsContainer;
78     private WidgetsListHeader mSuggestedWidgetsHeader;
79     private PackageUserKey mSuggestedWidgetsPackageUserKey;
80     private View mPrimaryWidgetListView;
81     private LinearLayout mRightPane;
82 
83     private ScrollView mRightPaneScrollView;
84     private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
85 
86     private boolean mOldIsSwipeToDismissInProgress;
87     private int mActivePage = -1;
88     @Nullable
89     private PackageUserKey mSelectedHeader;
90     private TextView mHeaderDescription;
91 
92     /**
93      * A menu displayed for options (e.g. "show all widgets" filter) around widget lists in the
94      * picker.
95      */
96     protected View mWidgetOptionsMenu;
97     /**
98      * State of the options in the menu (if displayed to the user).
99      */
100     @Nullable
101     protected WidgetOptionsMenuState mWidgetOptionsMenuState = null;
102 
WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr)103     public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
104         super(context, attrs, defStyleAttr);
105     }
106 
WidgetsTwoPaneSheet(Context context, AttributeSet attrs)107     public WidgetsTwoPaneSheet(Context context, AttributeSet attrs) {
108         super(context, attrs);
109     }
110 
111     @Override
setupSheet()112     protected void setupSheet() {
113         // Set the header change listener in the adapter
114         mAdapters.get(AdapterHolder.PRIMARY)
115                 .mWidgetsListAdapter.setHeaderChangeListener(getHeaderChangeListener());
116         mAdapters.get(AdapterHolder.WORK)
117                 .mWidgetsListAdapter.setHeaderChangeListener(getHeaderChangeListener());
118         mAdapters.get(AdapterHolder.SEARCH)
119                 .mWidgetsListAdapter.setHeaderChangeListener(getHeaderChangeListener());
120 
121         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
122 
123         int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_two_pane_sheet_paged_view
124                 : R.layout.widgets_two_pane_sheet_recyclerview;
125         layoutInflater.inflate(contentLayoutRes, findViewById(R.id.recycler_view_container), true);
126 
127         setupViews();
128 
129         mWidgetsListTableViewHolderBinder =
130                 new WidgetsListTableViewHolderBinder(mActivityContext, layoutInflater, this, this);
131 
132         mWidgetRecommendationsContainer = mContent.findViewById(
133                 R.id.widget_recommendations_container);
134         mWidgetRecommendationsView = mContent.findViewById(
135                 R.id.widget_recommendations_view);
136         mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
137         mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
138         mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
139         if (!mDeviceProfile.isTwoPanels) {
140             mWidgetRecommendationsView.enableFullPageViewIfLowDensity();
141         }
142         // To save the currently displayed page, so that, it can be requested when rebinding
143         // recommendations with different size constraints.
144         mWidgetRecommendationsView.addPageSwitchListener(
145                 newPage -> mRecommendationsCurrentPage = newPage);
146 
147         mHeaderTitle = mContent.findViewById(R.id.title);
148         mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
149 
150         mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu);
151         if (!enableTieredWidgetsByDefaultInPicker()) {
152             setupWidgetOptionsMenu();
153         }
154 
155         mRightPane = mContent.findViewById(R.id.right_pane);
156         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
157         mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
158 
159         mPrimaryWidgetListView = findViewById(R.id.primary_widgets_list_view);
160         mPrimaryWidgetListView.setOutlineProvider(mViewOutlineProvider);
161         mPrimaryWidgetListView.setClipToOutline(true);
162 
163         onWidgetsBound();
164 
165         // Set the fast scroller as not visible for two pane layout.
166         mFastScroller.setVisibility(GONE);
167     }
168 
169     @Override
mayUpdateTitleAndDescription(@ullable String title, @Nullable String description)170     public void mayUpdateTitleAndDescription(@Nullable String title, @Nullable String description) {
171         if (title != null) {
172             mHeaderTitle.setText(title);
173         }
174         if (description != null) {
175             mHeaderDescription.setText(description);
176             mHeaderDescription.setVisibility(VISIBLE);
177         }
178     }
179 
setupWidgetOptionsMenu()180     protected void setupWidgetOptionsMenu() {
181         mWidgetOptionsMenu.setOnClickListener(new OnClickListener() {
182             @Override
183             public void onClick(View v) {
184                 if (mWidgetOptionsMenuState != null) {
185                     PopupMenu popupMenu = new PopupMenu(mActivityContext, /*anchor=*/ v,
186                             Gravity.END);
187                     MenuItem menuItem = popupMenu.getMenu().add(
188                             R.string.widget_picker_show_all_widgets_menu_item_title);
189                     menuItem.setCheckable(true);
190                     menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
191                     menuItem.setOnMenuItemClickListener(
192                             item -> onShowAllWidgetsMenuItemClick(item));
193                     popupMenu.show();
194                 }
195             }
196         });
197     }
198 
onShowAllWidgetsMenuItemClick(MenuItem menuItem)199     private boolean onShowAllWidgetsMenuItemClick(MenuItem menuItem) {
200         mWidgetOptionsMenuState.showAllWidgets = !mWidgetOptionsMenuState.showAllWidgets;
201         menuItem.setChecked(mWidgetOptionsMenuState.showAllWidgets);
202 
203         // Refresh widgets
204         onWidgetsBound();
205         if (mIsInSearchMode) {
206             mSearchBar.reset();
207         } else if (!mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)) {
208             mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
209             mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop();
210         }
211         return true;
212     }
213 
214     @Override
getTabletHorizontalMargin(DeviceProfile deviceProfile)215     protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
216         // two pane picker is full width for fold as well as tablet.
217         return getResources().getDimensionPixelSize(
218                 R.dimen.widget_picker_two_panels_left_right_margin);
219     }
220 
221     @Override
onUserSwipeToDismissProgressChanged()222     protected void onUserSwipeToDismissProgressChanged() {
223         super.onUserSwipeToDismissProgressChanged();
224         boolean isSwipeToDismissInProgress = mSwipeToDismissProgress.value > 0;
225         if (isSwipeToDismissInProgress == mOldIsSwipeToDismissInProgress) {
226             return;
227         }
228         mOldIsSwipeToDismissInProgress = isSwipeToDismissInProgress;
229         if (isSwipeToDismissInProgress) {
230             modifyAttributesOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent,
231                     CLIP_CHILDREN_FALSE_MODIFIER);
232             modifyAttributesOnViewTree(mRightPaneScrollView,  (ViewParent) mContent,
233                     CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
234         } else {
235             restoreAttributesOnViewTree(mPrimaryWidgetListView, mContent,
236                     CLIP_CHILDREN_FALSE_MODIFIER);
237             restoreAttributesOnViewTree(mRightPaneScrollView, mContent,
238                     CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
239         }
240     }
241 
242     @Override
onLayout(boolean changed, int l, int t, int r, int b)243     protected void onLayout(boolean changed, int l, int t, int r, int b) {
244         super.onLayout(changed, l, t, r, b);
245         if (changed && mDeviceProfile.isTwoPanels) {
246             LinearLayout layout = mContent.findViewById(R.id.linear_layout_container);
247             FrameLayout leftPane = layout.findViewById(R.id.recycler_view_container);
248             LinearLayout.LayoutParams layoutParams = (LayoutParams) leftPane.getLayoutParams();
249             // Width is 1/3 of the sheet unless it's less than min width or max width
250             int leftPaneWidth = layout.getMeasuredWidth() / 3;
251             @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
252             @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
253             if (leftPaneWidth < minLeftPaneWidthPx) {
254                 layoutParams.width = minLeftPaneWidthPx;
255             } else if (leftPaneWidth > maxLeftPaneWidthPx) {
256                 layoutParams.width = maxLeftPaneWidthPx;
257             } else {
258                 layoutParams.width = 0;
259             }
260             layoutParams.weight = layoutParams.width == 0 ? 0.33F : 0;
261 
262             post(() -> {
263                 // The following calls all trigger requestLayout, so we post them to avoid
264                 // calling requestLayout during a layout pass. This also fixes the related warnings
265                 // in logcat.
266                 leftPane.setLayoutParams(layoutParams);
267                 requestApplyInsets();
268                 if (mSelectedHeader != null) {
269                     if (mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) {
270                         mSuggestedWidgetsHeader.callOnClick();
271                     } else {
272                         getHeaderChangeListener().onHeaderChanged(mSelectedHeader);
273                     }
274                 }
275             });
276         }
277     }
278 
279     // Used by the two pane sheet to show 3-dot menu to toggle between default lists and all lists
280     // when enableTieredWidgetsByDefaultInPicker is OFF. This code path and the 3-dot menu can be
281     // safely deleted when it's alternative "enableTieredWidgetsByDefaultInPicker" flag is inlined.
282     @Override
getWidgetsToDisplay()283     protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
284         List<WidgetsListBaseEntry> allWidgets =
285                 mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
286         List<WidgetsListBaseEntry> defaultWidgets =
287                 mActivityContext.getWidgetPickerDataProvider().get().getDefaultWidgets();
288 
289         if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) {
290             // no menu if there are no default widgets to show
291             mWidgetOptionsMenuState = null;
292             mWidgetOptionsMenu.setVisibility(GONE);
293         } else {
294             if (mWidgetOptionsMenuState == null) {
295                 mWidgetOptionsMenuState = new WidgetOptionsMenuState();
296             }
297 
298             mWidgetOptionsMenu.setVisibility(VISIBLE);
299             return mWidgetOptionsMenuState.showAllWidgets ? allWidgets : defaultWidgets;
300         }
301 
302         return allWidgets;
303     }
304 
305     @Override
onWidgetsBound()306     public void onWidgetsBound() {
307         super.onWidgetsBound();
308         if (mRecommendedWidgetsCount == 0 && mSelectedHeader == null) {
309             mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
310             mAdapters.get(mActivePage).mWidgetsRecyclerView.scrollToTop();
311         }
312     }
313 
314     @Override
onWidgetsListExpandButtonClick(View v)315     public void onWidgetsListExpandButtonClick(View v) {
316         super.onWidgetsListExpandButtonClick(v);
317         // Refresh right pane with updated data for the selected header.
318         if (mSelectedHeader != null && mSelectedHeader != mSuggestedWidgetsPackageUserKey) {
319             getHeaderChangeListener().onHeaderChanged(mSelectedHeader);
320         }
321     }
322 
323     @Override
onRecommendedWidgetsBound()324     public void onRecommendedWidgetsBound() {
325         super.onRecommendedWidgetsBound();
326 
327         if (mSuggestedWidgetsContainer == null && mRecommendedWidgetsCount > 0) {
328             setupSuggestedWidgets(LayoutInflater.from(getContext()));
329             mSuggestedWidgetsHeader.callOnClick();
330         } else if (mSelectedHeader != null
331                 && mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey)) {
332             // Reselect widget if we are reloading recommendations while it is currently showing.
333             selectWidgetCell(mWidgetRecommendationsContainer, getLastSelectedWidgetItem());
334         }
335     }
336 
setupSuggestedWidgets(LayoutInflater layoutInflater)337     private void setupSuggestedWidgets(LayoutInflater layoutInflater) {
338         // Add suggested widgets.
339         mSuggestedWidgetsContainer = mSearchScrollView.findViewById(R.id.suggestions_header);
340 
341         // Inflate the suggestions header.
342         mSuggestedWidgetsHeader = (WidgetsListHeader) layoutInflater.inflate(
343                 R.layout.widgets_list_row_header_two_pane,
344                 mSuggestedWidgetsContainer,
345                 false);
346         mSuggestedWidgetsHeader.setExpanded(true);
347 
348         PackageItemInfo packageItemInfo = new HighresPackageItemInfo(
349                 /* packageName= */ SUGGESTIONS_PACKAGE_NAME,
350                 Process.myUserHandle());
351         String suggestionsHeaderTitle = getContext().getString(
352                 R.string.suggested_widgets_header_title);
353         String suggestionsRightPaneTitle = getContext().getString(
354                 R.string.widget_picker_right_pane_accessibility_title, suggestionsHeaderTitle);
355         packageItemInfo.title = suggestionsHeaderTitle;
356         // Suggestions may update at run time. The widgets count on suggestions doesn't add any
357         // value, so, we don't show the count.
358         WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
359                         packageItemInfo,
360                         /*titleSectionName=*/ suggestionsHeaderTitle,
361                         /*items=*/ List.of(), // not necessary
362                         /*visibleWidgetsCount=*/ 0)
363                 .withWidgetListShown();
364 
365         mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry);
366         mSuggestedWidgetsHeader.setIcon(
367                 getContext().getDrawable(R.drawable.widget_suggestions_icon));
368         mSuggestedWidgetsHeader.setOnClickListener(view -> {
369             mSuggestedWidgetsHeader.setExpanded(true);
370             resetExpandedHeaders();
371             mRightPane.removeAllViews();
372             mRightPane.addView(mWidgetRecommendationsContainer);
373             mRightPaneScrollView.setScrollY(0);
374             mSuggestedWidgetsPackageUserKey = PackageUserKey.fromPackageItemInfo(packageItemInfo);
375             final boolean isChangingHeaders = mSelectedHeader == null
376                     || !mSelectedHeader.equals(mSuggestedWidgetsPackageUserKey);
377             if (isChangingHeaders)  {
378                 // If the initial focus view is still focused or widget picker is still opening, it
379                 // is likely a programmatic header click.
380                 if (mSelectedHeader != null && !mOpenCloseAnimation.getAnimationPlayer().isRunning()
381                         && !getAccessibilityInitialFocusView().isAccessibilityFocused()) {
382                     mRightPaneScrollView.setAccessibilityPaneTitle(suggestionsRightPaneTitle);
383                     focusOnFirstWidgetCell(mWidgetRecommendationsView);
384                 }
385                 // If switching from another header, unselect any WidgetCells. This is necessary
386                 // because we do not clear/recycle the WidgetCells in the recommendations container
387                 // when the header is clicked, only when onRecommendationsBound is called. That
388                 // means a WidgetCell in the recommendations container may still be selected from
389                 // the last time the recommendations were shown.
390                 unselectWidgetCell(mWidgetRecommendationsContainer, getLastSelectedWidgetItem());
391             }
392             mSelectedHeader = mSuggestedWidgetsPackageUserKey;
393         });
394         mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader);
395     }
396 
397     @Override
398     @Px
getMaxAvailableHeightForRecommendations()399     protected float getMaxAvailableHeightForRecommendations() {
400         if (mRecommendedWidgetsCount > 0) {
401             // If widgets were already selected for display, we show them all on orientation change
402             // in a two pane picker
403             return Float.MAX_VALUE;
404         }
405 
406         return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding)
407                 * RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE;
408     }
409 
410     @Override
411     @Px
getAvailableWidthForSuggestions(int pickerAvailableWidth)412     protected int getAvailableWidthForSuggestions(int pickerAvailableWidth) {
413         int rightPaneWidth = (int) Math.ceil(0.67 * pickerAvailableWidth);
414 
415         if (mDeviceProfile.isTwoPanels) {
416             // See onLayout
417             int leftPaneWidth = (int) (0.33 * pickerAvailableWidth);
418             @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
419             @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
420             if (leftPaneWidth < minLeftPaneWidthPx) {
421                 leftPaneWidth = minLeftPaneWidthPx;
422             } else if (leftPaneWidth > maxLeftPaneWidthPx) {
423                 leftPaneWidth = maxLeftPaneWidthPx;
424             }
425             rightPaneWidth = pickerAvailableWidth - leftPaneWidth;
426         }
427 
428         // Since suggestions are shown in right pane, the available width is 2/3 of total width of
429         // bottom sheet.
430         return rightPaneWidth - getResources().getDimensionPixelSize(
431                 R.dimen.widget_list_horizontal_margin_two_pane); // right pane end margin.
432     }
433 
434     @Override
onActivePageChanged(int currentActivePage)435     public void onActivePageChanged(int currentActivePage) {
436         super.onActivePageChanged(currentActivePage);
437 
438         // If active page didn't change then we don't want to update the header.
439         if (mActivePage == currentActivePage) {
440             return;
441         }
442 
443         mActivePage = currentActivePage;
444 
445         // When using talkback, swiping left while on right pane, should navigate to the widgets
446         // list on left.
447         mAdapters.get(mActivePage).mWidgetsRecyclerView.setAccessibilityTraversalBefore(
448                 mRightPaneScrollView.getId());
449 
450         // On page change, select the first item in the list to show in the right pane.
451         mAdapters.get(currentActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
452         mAdapters.get(currentActivePage).mWidgetsRecyclerView.scrollToTop();
453     }
454 
455     @Override
updateRecyclerViewVisibility(AdapterHolder adapterHolder)456     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
457         // The first item is always an empty space entry. Look for any more items.
458         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
459         if (!isWidgetAvailable) {
460             mRightPane.removeAllViews();
461             mRightPane.addView(mNoWidgetsView);
462             // with no widgets message, no header is selected on left
463             if (mSuggestedWidgetsPackageUserKey != null
464                     && mSuggestedWidgetsPackageUserKey.equals(mSelectedHeader)
465                     && mSuggestedWidgetsHeader != null) {
466                 mSuggestedWidgetsHeader.setExpanded(false);
467             }
468             mSelectedHeader = null;
469         }
470         super.updateRecyclerViewVisibility(adapterHolder);
471     }
472 
473     @Override
onSearchResults(List<WidgetsListBaseEntry> entries)474     public void onSearchResults(List<WidgetsListBaseEntry> entries) {
475         super.onSearchResults(entries);
476         mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.selectFirstHeaderEntry();
477         mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
478     }
479 
480     @Override
shouldScroll(MotionEvent ev)481     protected boolean shouldScroll(MotionEvent ev) {
482         return getPopupContainer().isEventOverView(mRightPaneScrollView, ev)
483                 ? mRightPaneScrollView.canScrollVertically(-1)
484                 : super.shouldScroll(ev);
485     }
486 
487     @Override
setViewVisibilityBasedOnSearch(boolean isInSearchMode)488     protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
489         super.setViewVisibilityBasedOnSearch(isInSearchMode);
490 
491         if (mSuggestedWidgetsHeader != null && mSuggestedWidgetsContainer != null) {
492             if (!isInSearchMode) {
493                 mSuggestedWidgetsContainer.setVisibility(VISIBLE);
494                 mSuggestedWidgetsHeader.callOnClick();
495             } else {
496                 mSuggestedWidgetsContainer.setVisibility(GONE);
497             }
498         } else if (!isInSearchMode) {
499             mAdapters.get(mActivePage).mWidgetsListAdapter.selectFirstHeaderEntry();
500         }
501 
502     }
503 
getHeaderChangeListener()504     private HeaderChangeListener getHeaderChangeListener() {
505         return new HeaderChangeListener() {
506             @Override
507             public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
508                 final boolean isSameHeader = mSelectedHeader != null
509                         && mSelectedHeader.equals(selectedHeader);
510                 // If the initial focus view is still focused or widget picker is still opening, it
511                 // is likely a programmatic header click.
512                 final boolean isUserClick = mSelectedHeader != null
513                         && !mOpenCloseAnimation.getAnimationPlayer().isRunning()
514                         && !getAccessibilityInitialFocusView().isAccessibilityFocused();
515                 mSelectedHeader = selectedHeader;
516 
517                 WidgetsListContentEntry contentEntry;
518                 if (enableTieredWidgetsByDefaultInPicker()) {
519                     contentEntry = mAdapters.get(
520                             getCurrentAdapterHolderType()).mWidgetsListAdapter.getContentEntry(
521                             selectedHeader);
522                 } else { // Can be deleted when inlining the "enableTieredWidgetsByDefaultInPicker"
523                     // flag
524                     final boolean showDefaultWidgets = mWidgetOptionsMenuState != null
525                             && !mWidgetOptionsMenuState.showAllWidgets;
526                     contentEntry = findContentEntryForPackageUser(
527                             mActivityContext.getWidgetPickerDataProvider().get(),
528                             selectedHeader, showDefaultWidgets);
529                 }
530 
531                 if (contentEntry == null || mRightPane == null) {
532                     return;
533                 }
534 
535                 if (mSuggestedWidgetsHeader != null) {
536                     mSuggestedWidgetsHeader.setExpanded(false);
537                 }
538 
539                 WidgetsListContentEntry contentEntryToBind;
540                 // Setting max span size enables row to understand how to fit more than one item
541                 // in a row.
542                 contentEntryToBind = contentEntry.withMaxSpanSize(mMaxSpanPerRow);
543 
544                 WidgetsRowViewHolder widgetsRowViewHolder =
545                         mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane);
546                 mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
547                         contentEntryToBind,
548                         ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
549                         Collections.EMPTY_LIST);
550                 if (isSameHeader) {
551                     // Reselect the last selected widget if we are reloading the same header.
552                     selectWidgetCell(widgetsRowViewHolder.tableContainer,
553                             getLastSelectedWidgetItem());
554                 }
555                 widgetsRowViewHolder.mDataCallback = data -> {
556                     mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
557                             contentEntryToBind,
558                             ViewHolderBinder.POSITION_FIRST | ViewHolderBinder.POSITION_LAST,
559                             Collections.singletonList(data));
560                     if (isSameHeader) {
561                         selectWidgetCell(widgetsRowViewHolder.tableContainer,
562                                 getLastSelectedWidgetItem());
563                     }
564                 };
565                 mRightPane.removeAllViews();
566                 mRightPane.addView(widgetsRowViewHolder.itemView);
567                 if (isUserClick) {
568                     mRightPaneScrollView.setAccessibilityPaneTitle(getContext().getString(
569                             R.string.widget_picker_right_pane_accessibility_title,
570                             contentEntry.mPkgItem.title));
571                     postDelayed(() -> focusOnFirstWidgetCell(widgetsRowViewHolder.tableContainer),
572                             WIDGET_LIST_ITEM_APPEARANCE_DELAY);
573                 }
574                 mRightPaneScrollView.setScrollY(0);
575             }
576         };
577     }
578 
579     private static void selectWidgetCell(ViewGroup parent, WidgetItem item) {
580         if (parent == null || item == null) return;
581         WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc
582                 && wc.matchesItem(item));
583         if (cell != null && !cell.isShowingAddButton()) {
584             cell.callOnClick();
585         }
586     }
587 
588     /**
589      * Requests focus on the first widget cell in the given widget section.
590      */
591     private static void focusOnFirstWidgetCell(ViewGroup parent) {
592         if (parent == null) return;
593         WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell);
594         if (cell != null) {
595             cell.performAccessibilityAction(
596                     AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
597         }
598     }
599 
600     private static void unselectWidgetCell(ViewGroup parent, WidgetItem item) {
601         if (parent == null || item == null) return;
602         WidgetCell cell = Utilities.findViewByPredicate(parent, v -> v instanceof WidgetCell wc
603                 && wc.matchesItem(item));
604         if (cell != null && cell.isShowingAddButton()) {
605             cell.hideAddButton(/* animate= */ false);
606         }
607     }
608 
609     @Override
610     public void setInsets(Rect insets) {
611         super.setInsets(insets);
612         FrameLayout rightPaneContainer = mContent.findViewById(R.id.right_pane_container);
613         rightPaneContainer.setPadding(
614                 rightPaneContainer.getPaddingLeft(),
615                 rightPaneContainer.getPaddingTop(),
616                 rightPaneContainer.getPaddingRight(),
617                 mBottomPadding);
618         requestLayout();
619     }
620 
621     @Override
622     protected int getWidgetListHorizontalMargin() {
623         return getResources().getDimensionPixelSize(
624                 R.dimen.widget_list_left_pane_horizontal_margin);
625     }
626 
627     @Override
628     protected boolean isTwoPane() {
629         return true;
630     }
631 
632     @Override
633     protected int getHeaderTopClip(@NonNull WidgetCell cell) {
634         return 0;
635     }
636 
637     @Override
638     protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) {
639         for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) {
640             if (parent instanceof ScrollView scrollView) {
641                 scrollView.smoothScrollBy(0, scrollByY);
642                 return;
643             } else if (parent == this) {
644                 return;
645             }
646         }
647     }
648 
649     /**
650      * This is a listener for when the selected header gets changed in the left pane.
651      */
652     public interface HeaderChangeListener {
653         /**
654          * Sets the right pane to have the widgets for the currently selected header from
655          * the left pane.
656          */
657         void onHeaderChanged(@NonNull PackageUserKey selectedHeader);
658     }
659 
660     /**
661      * Holds the selection state of the options menu (if presented to the user).
662      */
663     protected static class WidgetOptionsMenuState {
664         /**
665          * UI state indicating whether to show default or all widgets.
666          * <p>If true, shows all widgets; else shows the default widgets.</p>
667          */
668         public boolean showAllWidgets = false;
669     }
670 
671     private static class HighresPackageItemInfo extends PackageItemInfo {
672         HighresPackageItemInfo(String packageName, UserHandle user) {
673             super(packageName, user);
674         }
675 
676         @Override
677         public CacheLookupFlag getMatchingLookupFlag() {
678             return DEFAULT_LOOKUP_FLAG;
679         }
680     }
681 }
682