1 /*
2  * Copyright (C) 2017 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.Flags.enableUnfoldedTwoPanePicker;
20 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
22 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
23 
24 import android.animation.Animator;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.graphics.Rect;
28 import android.os.Bundle;
29 import android.os.Parcelable;
30 import android.os.Process;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.util.AttributeSet;
34 import android.util.Pair;
35 import android.util.SparseArray;
36 import android.view.LayoutInflater;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.view.ViewParent;
41 import android.view.WindowInsets;
42 import android.view.WindowInsetsController;
43 import android.view.animation.AnimationUtils;
44 import android.view.animation.Interpolator;
45 import android.widget.Button;
46 import android.widget.LinearLayout;
47 import android.widget.TextView;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 import androidx.annotation.Px;
52 import androidx.annotation.VisibleForTesting;
53 import androidx.recyclerview.widget.DefaultItemAnimator;
54 import androidx.recyclerview.widget.RecyclerView;
55 
56 import com.android.launcher3.BaseActivity;
57 import com.android.launcher3.DeviceProfile;
58 import com.android.launcher3.LauncherAppState;
59 import com.android.launcher3.R;
60 import com.android.launcher3.anim.PendingAnimation;
61 import com.android.launcher3.compat.AccessibilityManagerCompat;
62 import com.android.launcher3.model.UserManagerState;
63 import com.android.launcher3.model.WidgetItem;
64 import com.android.launcher3.pm.UserCache;
65 import com.android.launcher3.views.RecyclerViewFastScroller;
66 import com.android.launcher3.views.SpringRelativeLayout;
67 import com.android.launcher3.views.StickyHeaderLayout;
68 import com.android.launcher3.widget.BaseWidgetSheet;
69 import com.android.launcher3.widget.WidgetCell;
70 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
71 import com.android.launcher3.widget.picker.search.SearchModeListener;
72 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
73 import com.android.launcher3.workprofile.PersonalWorkPagedView;
74 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
75 
76 import java.util.ArrayList;
77 import java.util.HashMap;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.function.Predicate;
81 import java.util.stream.IntStream;
82 
83 /**
84  * Popup for showing the full list of available widgets
85  */
86 public class WidgetsFullSheet extends BaseWidgetSheet
87         implements OnActivePageChangedListener,
88         WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
89 
90     private static final long FADE_IN_DURATION = 150;
91 
92     // The widget recommendation table can easily take over the entire screen on devices with small
93     // resolution or landscape on phone. This ratio defines the max percentage of content area that
94     // the table can display with respect to bottom sheet's height.
95     private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.45f;
96     private static final String RECOMMENDATIONS_SAVED_STATE_KEY =
97             "widgetsFullSheet:mRecommendationsCurrentPage";
98     private static final String SUPER_SAVED_STATE_KEY = "widgetsFullSheet:superHierarchyState";
99     private final UserCache mUserCache;
100     private final UserManagerState mUserManagerState = new UserManagerState();
101     private final UserHandle mCurrentUser = Process.myUserHandle();
102     private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter =
103             entry -> mCurrentUser.equals(entry.mPkgItem.user);
104     private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter;
105     protected final boolean mHasWorkProfile;
106     // Number of recommendations displayed
107     protected int mRecommendedWidgetsCount;
108     private List<WidgetItem> mRecommendedWidgets = new ArrayList<>();
109     private Map<WidgetRecommendationCategory, List<WidgetItem>> mRecommendedWidgetsMap =
110             new HashMap<>();
111     protected int mRecommendationsCurrentPage = 0;
112     protected final SparseArray<AdapterHolder> mAdapters = new SparseArray();
113 
114     private final OnAttachStateChangeListener mBindScrollbarInSearchMode =
115             new OnAttachStateChangeListener() {
116                 @Override
117                 public void onViewAttachedToWindow(View view) {
118                     WidgetsRecyclerView searchRecyclerView =
119                             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
120                     if (mIsInSearchMode && searchRecyclerView != null) {
121                         searchRecyclerView.bindFastScrollbar(mFastScroller);
122                     }
123                 }
124 
125                 @Override
126                 public void onViewDetachedFromWindow(View view) {
127                 }
128             };
129 
130     @Px
131     private final int mTabsHeight;
132 
133     @Nullable
134     private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
135     @Nullable
136     private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
137     @Nullable
138     PersonalWorkPagedView mViewPager;
139     private boolean mIsInSearchMode;
140     private boolean mIsNoWidgetsViewNeeded;
141     @Px
142     protected int mMaxSpanPerRow;
143     protected DeviceProfile mDeviceProfile;
144 
145     protected TextView mNoWidgetsView;
146     protected StickyHeaderLayout mSearchScrollView;
147     protected WidgetRecommendationsView mWidgetRecommendationsView;
148     protected LinearLayout mWidgetRecommendationsContainer;
149     protected View mTabBar;
150     protected View mSearchBarContainer;
151     protected WidgetsSearchBar mSearchBar;
152     protected TextView mHeaderTitle;
153     protected RecyclerViewFastScroller mFastScroller;
154     protected int mBottomPadding;
155 
WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr)156     public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
157         super(context, attrs, defStyleAttr);
158         mDeviceProfile = mActivityContext.getDeviceProfile();
159         mUserCache = UserCache.INSTANCE.get(context);
160         mHasWorkProfile = mUserCache.getUserProfiles()
161                 .stream()
162                 .anyMatch(user -> mUserCache.getUserInfo(user).isWork());
163         mWorkWidgetsFilter = entry -> mHasWorkProfile
164                 && mUserCache.getUserInfo(entry.mPkgItem.user).isWork()
165                 && !mUserManagerState.isUserQuiet(entry.mPkgItem.user);
166         mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
167         mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
168         mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
169 
170         Resources resources = getResources();
171         mUserManagerState.init(UserCache.INSTANCE.get(context),
172                 context.getSystemService(UserManager.class));
173         mTabsHeight = mHasWorkProfile
174                 ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
175                 : 0;
176     }
177 
WidgetsFullSheet(Context context, AttributeSet attrs)178     public WidgetsFullSheet(Context context, AttributeSet attrs) {
179         this(context, attrs, 0);
180     }
181 
182     @Override
onFinishInflate()183     protected void onFinishInflate() {
184         super.onFinishInflate();
185 
186         mContent = findViewById(R.id.container);
187         setContentBackgroundWithParent(getContext().getDrawable(R.drawable.bg_widgets_full_sheet),
188                 mContent);
189         mContent.setOutlineProvider(mViewOutlineProvider);
190         mContent.setClipToOutline(true);
191         setupSheet();
192     }
193 
setupSheet()194     protected void setupSheet() {
195         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
196         int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
197                 : R.layout.widgets_full_sheet_recyclerview;
198         layoutInflater.inflate(contentLayoutRes, mContent, true);
199 
200         setupViews();
201 
202         mWidgetRecommendationsContainer = mSearchScrollView.findViewById(
203                 R.id.widget_recommendations_container);
204         mWidgetRecommendationsView = mSearchScrollView.findViewById(
205                 R.id.widget_recommendations_view);
206         // To save the currently displayed page, so that, it can be requested when rebinding
207         // recommendations with different size constraints.
208         mWidgetRecommendationsView.addPageSwitchListener(
209                 newPage -> mRecommendationsCurrentPage = newPage);
210         mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
211         mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
212         mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
213 
214         mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
215 
216         onWidgetsBound();
217     }
218 
setupViews()219     protected void setupViews() {
220         mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
221         mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
222         mNoWidgetsView = findViewById(R.id.no_widgets_text);
223         mFastScroller = findViewById(R.id.fast_scroller);
224         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
225         mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view));
226         mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view));
227         if (mHasWorkProfile) {
228             mViewPager = findViewById(R.id.widgets_view_pager);
229             mViewPager.setOutlineProvider(mViewOutlineProvider);
230             mViewPager.setClipToOutline(true);
231             mViewPager.setClipChildren(false);
232             mViewPager.initParentViews(this);
233             mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
234             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
235             findViewById(R.id.tab_personal)
236                     .setOnClickListener((View view) -> mViewPager.snapToPage(0));
237             findViewById(R.id.tab_work)
238                     .setOnClickListener((View view) -> mViewPager.snapToPage(1));
239             mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view));
240             setDeviceManagementResources();
241         } else {
242             mViewPager = null;
243         }
244 
245         mTabBar = mSearchScrollView.findViewById(R.id.tabs);
246         mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
247         mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
248 
249         mSearchBar.initialize(
250                 mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
251     }
252 
setDeviceManagementResources()253     private void setDeviceManagementResources() {
254         if (mActivityContext.getStringCache() != null) {
255             Button personalTab = findViewById(R.id.tab_personal);
256             personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab);
257 
258             Button workTab = findViewById(R.id.tab_work);
259             workTab.setText(mActivityContext.getStringCache().widgetsWorkTab);
260         }
261     }
262 
263     @Override
onActivePageChanged(int currentActivePage)264     public void onActivePageChanged(int currentActivePage) {
265         AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
266         WidgetsRecyclerView currentRecyclerView =
267                 mAdapters.get(currentActivePage).mWidgetsRecyclerView;
268 
269         updateRecyclerViewVisibility(currentAdapterHolder);
270         attachScrollbarToRecyclerView(currentRecyclerView);
271     }
272 
attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView)273     private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
274         recyclerView.bindFastScrollbar(mFastScroller);
275         if (mCurrentWidgetsRecyclerView != recyclerView) {
276             // Only reset the scroll position & expanded apps if the currently shown recycler view
277             // has been updated.
278             reset();
279             resetExpandedHeaders();
280             mCurrentWidgetsRecyclerView = recyclerView;
281             mSearchScrollView.setCurrentRecyclerView(recyclerView);
282         }
283     }
284 
updateRecyclerViewVisibility(AdapterHolder adapterHolder)285     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
286         // The first item is always an empty space entry. Look for any more items.
287         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
288         adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
289 
290         if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) {
291             mNoWidgetsView.setText(R.string.no_search_results);
292         } else if (adapterHolder.mAdapterType == AdapterHolder.WORK
293                 && mUserCache.getUserProfiles().stream()
294                 .filter(userHandle -> mUserCache.getUserInfo(userHandle).isWork())
295                 .anyMatch(mUserManagerState::isUserQuiet)
296                 && mActivityContext.getStringCache() != null) {
297             mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle);
298         } else {
299             mNoWidgetsView.setText(R.string.no_widgets_available);
300         }
301         mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE);
302     }
303 
reset()304     private void reset() {
305         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
306         if (mHasWorkProfile) {
307             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
308         }
309         mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
310         mSearchScrollView.reset(/* animate= */ true);
311     }
312 
313     @VisibleForTesting
getRecyclerView()314     public WidgetsRecyclerView getRecyclerView() {
315         if (mIsInSearchMode) {
316             return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
317         }
318         if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
319             return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
320         }
321         return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView;
322     }
323 
324     @Override
getAccessibilityTarget()325     protected Pair<View, String> getAccessibilityTarget() {
326         return Pair.create(getRecyclerView(), getContext().getString(
327                 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
328     }
329 
330     @Override
onAttachedToWindow()331     protected void onAttachedToWindow() {
332         super.onAttachedToWindow();
333         onWidgetsBound();
334     }
335 
336     @Override
onDetachedFromWindow()337     protected void onDetachedFromWindow() {
338         super.onDetachedFromWindow();
339         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
340                 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
341         if (mHasWorkProfile) {
342             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView
343                     .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
344         }
345     }
346 
347     @Override
setInsets(Rect insets)348     public void setInsets(Rect insets) {
349         super.setInsets(insets);
350         mBottomPadding = Math.max(insets.bottom, mNavBarScrimHeight);
351         setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, mBottomPadding);
352         setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, mBottomPadding);
353         if (mHasWorkProfile) {
354             setBottomPadding(mAdapters.get(AdapterHolder.WORK)
355                     .mWidgetsRecyclerView, mBottomPadding);
356         }
357         ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = mBottomPadding;
358 
359         if (mBottomPadding > 0) {
360             setupNavBarColor();
361         } else {
362             clearNavBarColor();
363         }
364 
365         requestLayout();
366     }
367 
368     @Override
onApplyWindowInsets(WindowInsets insets)369     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
370         WindowInsets w = super.onApplyWindowInsets(insets);
371         if (mInsets.bottom != mNavBarScrimHeight) {
372             setInsets(mInsets);
373         }
374         return w;
375     }
376 
setBottomPadding(RecyclerView recyclerView, int bottomPadding)377     private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) {
378         recyclerView.setPadding(
379                 recyclerView.getPaddingLeft(),
380                 recyclerView.getPaddingTop(),
381                 recyclerView.getPaddingRight(),
382                 bottomPadding);
383     }
384 
385     @Override
onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)386     protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) {
387         setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx);
388         if (mViewPager == null) {
389             setContentViewChildHorizontalPadding(
390                     mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
391                     contentHorizontalMarginInPx);
392         } else {
393             setContentViewChildHorizontalPadding(
394                     mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
395                     contentHorizontalMarginInPx);
396             setContentViewChildHorizontalPadding(
397                     mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView,
398                     contentHorizontalMarginInPx);
399         }
400         setContentViewChildHorizontalPadding(
401                 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView,
402                 contentHorizontalMarginInPx);
403     }
404 
setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx)405     private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) {
406         ViewGroup.MarginLayoutParams layoutParams =
407                 (ViewGroup.MarginLayoutParams) view.getLayoutParams();
408         layoutParams.setMarginStart(horizontalMarginInPx);
409         layoutParams.setMarginEnd(horizontalMarginInPx);
410     }
411 
setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx)412     private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) {
413         view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx,
414                 view.getPaddingBottom());
415     }
416 
417     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)418     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
419         doMeasure(widthMeasureSpec, heightMeasureSpec);
420 
421         if (updateMaxSpansPerRow()) {
422             doMeasure(widthMeasureSpec, heightMeasureSpec);
423         }
424     }
425 
426     /** Returns {@code true} if the max spans have been updated. */
updateMaxSpansPerRow()427     private boolean updateMaxSpansPerRow() {
428         if (getMeasuredWidth() == 0) return false;
429 
430         @Px int maxHorizontalSpan = getContentView().getMeasuredWidth()
431                 - (2 * mContentHorizontalMargin);
432         if (mMaxSpanPerRow != maxHorizontalSpan) {
433             mMaxSpanPerRow = maxHorizontalSpan;
434             mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
435                     maxHorizontalSpan);
436             mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
437                     maxHorizontalSpan);
438             if (mHasWorkProfile) {
439                 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
440                         maxHorizontalSpan);
441             }
442             onRecommendedWidgetsBound();
443             return true;
444         }
445         return false;
446     }
447 
getContentView()448     protected View getContentView() {
449         return mHasWorkProfile
450                 ? mViewPager
451                 : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
452     }
453 
454     @Override
onLayout(boolean changed, int l, int t, int r, int b)455     protected void onLayout(boolean changed, int l, int t, int r, int b) {
456         int width = r - l;
457         int height = b - t;
458 
459         // Content is laid out as center bottom aligned
460         int contentWidth = mContent.getMeasuredWidth();
461         int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
462         mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
463                 contentLeft + contentWidth, height);
464 
465         setTranslationShift(mTranslationShift);
466     }
467 
468     @Override
onWidgetsBound()469     public void onWidgetsBound() {
470         if (mIsInSearchMode) {
471             return;
472         }
473         List<WidgetsListBaseEntry> allWidgets =
474                 mActivityContext.getPopupDataProvider().getAllWidgets();
475 
476         AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
477         primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
478 
479         if (mHasWorkProfile) {
480             mViewPager.setVisibility(VISIBLE);
481             mTabBar.setVisibility(VISIBLE);
482             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
483             workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
484             onActivePageChanged(mViewPager.getCurrentPage());
485         } else {
486             onActivePageChanged(0);
487         }
488         // Update recommended widgets section so that it occupies appropriate space on screen to
489         // leave enough space for presence/absence of mNoWidgetsView.
490         boolean isNoWidgetsViewNeeded =
491                 !mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries()
492                         || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK)
493                         .mWidgetsListAdapter.hasVisibleEntries());
494         if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) {
495             mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded;
496             onRecommendedWidgetsBound();
497         }
498     }
499 
500     @Override
enterSearchMode(boolean shouldLog)501     public void enterSearchMode(boolean shouldLog) {
502         if (mIsInSearchMode) return;
503         setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
504         attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
505         if (shouldLog) {
506             mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED);
507         }
508     }
509 
510     @Override
exitSearchMode()511     public void exitSearchMode() {
512         if (!mIsInSearchMode) return;
513         onSearchResults(new ArrayList<>());
514         WidgetsRecyclerView searchRecyclerView = mAdapters.get(
515                 AdapterHolder.SEARCH).mWidgetsRecyclerView;
516         // Remove all views when exiting the search mode; this prevents animating from stale results
517         // to new ones the next time we enter search mode. By the time recycler view is hidden,
518         // layout may not have happened to clear up existing results. So, instead of waiting for it
519         // to happen, we clear the views here.
520         searchRecyclerView.swapAdapter(
521                 searchRecyclerView.getAdapter(), /*removeAndRecycleExistingViews=*/ true);
522         setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
523         if (mHasWorkProfile) {
524             mViewPager.snapToPage(AdapterHolder.PRIMARY);
525         }
526         attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
527     }
528 
529     @Override
onSearchResults(List<WidgetsListBaseEntry> entries)530     public void onSearchResults(List<WidgetsListBaseEntry> entries) {
531         mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
532         updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
533     }
534 
setViewVisibilityBasedOnSearch(boolean isInSearchMode)535     protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
536         mIsInSearchMode = isInSearchMode;
537         if (isInSearchMode) {
538             mWidgetRecommendationsContainer.setVisibility(GONE);
539             if (mHasWorkProfile) {
540                 mViewPager.setVisibility(GONE);
541                 mTabBar.setVisibility(GONE);
542             } else {
543                 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE);
544             }
545             updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
546             // Hide no search results view to prevent it from flashing on enter search.
547             mNoWidgetsView.setVisibility(GONE);
548         } else {
549             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
550             // Visibility of recommended widgets, recycler views and headers are handled in methods
551             // below.
552             onRecommendedWidgetsBound();
553             onWidgetsBound();
554         }
555     }
556 
resetExpandedHeaders()557     protected void resetExpandedHeaders() {
558         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader();
559         mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
560     }
561 
562     @Override
onRecommendedWidgetsBound()563     public void onRecommendedWidgetsBound() {
564         if (mIsInSearchMode) {
565             return;
566         }
567 
568         if (enableCategorizedWidgetSuggestions()) {
569             // We avoid applying new recommendations when some are already displayed.
570             if (mRecommendedWidgetsMap.isEmpty()) {
571                 mRecommendedWidgetsMap =
572                         mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets();
573             }
574             mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
575                     mRecommendedWidgetsMap,
576                     mDeviceProfile,
577                     /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
578                     /* availableWidth= */ mMaxSpanPerRow,
579                     /* cellPadding= */ mWidgetCellHorizontalPadding,
580                     /* requestedPage= */ mRecommendationsCurrentPage
581             );
582         } else {
583             if (mRecommendedWidgets.isEmpty()) {
584                 mRecommendedWidgets =
585                         mActivityContext.getPopupDataProvider().getRecommendedWidgets();
586             }
587             mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
588                     mRecommendedWidgets,
589                     mDeviceProfile,
590                     /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
591                     /* availableWidth= */ mMaxSpanPerRow,
592                     /* cellPadding= */ mWidgetCellHorizontalPadding
593             );
594         }
595         mWidgetRecommendationsContainer.setVisibility(
596                 mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
597     }
598 
599     @Px
getMaxAvailableHeightForRecommendations()600     protected float getMaxAvailableHeightForRecommendations() {
601         // There isn't enough space to show recommendations in landscape orientation on phones with
602         // a full sheet design. Tablets use a two pane picker.
603         if (mDeviceProfile.isLandscape) {
604             return 0f;
605         }
606 
607         return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding)
608                 * RECOMMENDATION_TABLE_HEIGHT_RATIO;
609     }
610 
611     /** b/209579563: "Widgets" header should be focused first. */
612     @Override
getAccessibilityInitialFocusView()613     protected View getAccessibilityInitialFocusView() {
614         return mHeaderTitle;
615     }
616 
open(boolean animate)617     private void open(boolean animate) {
618         if (animate) {
619             if (getPopupContainer().getInsets().bottom > 0) {
620                 mContent.setAlpha(0);
621             }
622             setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration);
623             Animator animator = mOpenCloseAnimation.getAnimationPlayer();
624             animator.setInterpolator(AnimationUtils.loadInterpolator(
625                     getContext(), android.R.interpolator.linear_out_slow_in));
626             post(() -> {
627                 animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
628                         .start();
629                 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
630             });
631         } else {
632             setTranslationShift(TRANSLATION_SHIFT_OPENED);
633             post(this::announceAccessibilityChanges);
634         }
635     }
636 
637     @Override
handleClose(boolean animate)638     protected void handleClose(boolean animate) {
639         handleClose(animate, mActivityContext.getDeviceProfile().bottomSheetCloseDuration);
640     }
641 
642     @Override
isOfType(int type)643     protected boolean isOfType(int type) {
644         return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
645     }
646 
647     @Override
onControllerInterceptTouchEvent(MotionEvent ev)648     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
649         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
650             mNoIntercept = shouldScroll(ev);
651             if (mSearchBar.isSearchBarFocused()
652                     && !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) {
653                 mSearchBar.clearSearchBarFocus();
654             }
655         }
656 
657         return super.onControllerInterceptTouchEvent(ev);
658     }
659 
shouldScroll(MotionEvent ev)660     protected boolean shouldScroll(MotionEvent ev) {
661         boolean intercept = false;
662         WidgetsRecyclerView recyclerView = getRecyclerView();
663         RecyclerViewFastScroller scroller = recyclerView.getScrollbar();
664         // Disable swipe down when recycler view is scrolling
665         if (scroller.getThumbOffsetY() >= 0 && getPopupContainer().isEventOverView(scroller, ev)) {
666             intercept = true;
667         } else if (getPopupContainer().isEventOverView(recyclerView, ev)) {
668             intercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer());
669         }
670         return intercept;
671     }
672 
673     /** Shows the {@link WidgetsFullSheet} on the launcher. */
show(BaseActivity activity, boolean animate)674     public static WidgetsFullSheet show(BaseActivity activity, boolean animate) {
675         WidgetsFullSheet sheet = (WidgetsFullSheet) activity.getLayoutInflater().inflate(
676                 getWidgetSheetId(activity),
677                 activity.getDragLayer(),
678                 false);
679         sheet.attachToContainer();
680         sheet.mIsOpen = true;
681         sheet.open(animate);
682         return sheet;
683     }
684 
685     @Override
saveHierarchyState(SparseArray<Parcelable> sparseArray)686     public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
687         Bundle bundle = new Bundle();
688         // With widget picker open, when we open shade to switch theme, Launcher re-creates the
689         // picker and calls save/restore hierarchy state. We save the state of recommendations
690         // across those updates.
691         bundle.putInt(RECOMMENDATIONS_SAVED_STATE_KEY, mRecommendationsCurrentPage);
692         mWidgetRecommendationsView.saveState(bundle);
693         SparseArray<Parcelable> superState = new SparseArray<>();
694         super.saveHierarchyState(superState);
695         bundle.putSparseParcelableArray(SUPER_SAVED_STATE_KEY, superState);
696         sparseArray.put(0, bundle);
697     }
698 
699     @Override
restoreHierarchyState(SparseArray<Parcelable> sparseArray)700     public void restoreHierarchyState(SparseArray<Parcelable> sparseArray) {
701         Bundle state = (Bundle) sparseArray.get(0);
702         mRecommendationsCurrentPage = state.getInt(
703                 RECOMMENDATIONS_SAVED_STATE_KEY, /*defaultValue=*/0);
704         mWidgetRecommendationsView.restoreState(state);
705         super.restoreHierarchyState(state.getSparseParcelableArray(SUPER_SAVED_STATE_KEY));
706     }
707 
getWidgetSheetId(BaseActivity activity)708     private static int getWidgetSheetId(BaseActivity activity) {
709         boolean isTwoPane = (activity.getDeviceProfile().isTablet
710                 // Enables two pane picker for tablets in all orientations when the
711                 // enableCategorizedWidgetSuggestions flag is on.
712                 && (activity.getDeviceProfile().isLandscape || enableCategorizedWidgetSuggestions())
713                 && !activity.getDeviceProfile().isTwoPanels)
714                 // Enables two pane picker for unfolded foldables if the flag is on.
715                 || (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker());
716 
717         return isTwoPane ? R.layout.widgets_two_pane_sheet : R.layout.widgets_full_sheet;
718     }
719 
720     @Override
onInterceptTouchEvent(MotionEvent ev)721     public boolean onInterceptTouchEvent(MotionEvent ev) {
722         return isTouchOnScrollbar(ev) || super.onInterceptTouchEvent(ev);
723     }
724 
725     @Override
onTouchEvent(MotionEvent ev)726     public boolean onTouchEvent(MotionEvent ev) {
727         return maybeHandleTouchEvent(ev) || super.onTouchEvent(ev);
728     }
729 
maybeHandleTouchEvent(MotionEvent ev)730     private boolean maybeHandleTouchEvent(MotionEvent ev) {
731         boolean isEventHandled = false;
732 
733         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
734             mCurrentTouchEventRecyclerView = isTouchOnScrollbar(ev) ? getRecyclerView() : null;
735         }
736 
737         if (mCurrentTouchEventRecyclerView != null) {
738             final float offsetX = mContent.getX();
739             final float offsetY = mContent.getY();
740             ev.offsetLocation(-offsetX, -offsetY);
741             isEventHandled = mCurrentTouchEventRecyclerView.dispatchTouchEvent(ev);
742             ev.offsetLocation(offsetX, offsetY);
743         }
744 
745         if (ev.getAction() == MotionEvent.ACTION_UP
746                 || ev.getAction() == MotionEvent.ACTION_CANCEL) {
747             mCurrentTouchEventRecyclerView = null;
748         }
749 
750         return isEventHandled;
751     }
752 
isTouchOnScrollbar(MotionEvent ev)753     private boolean isTouchOnScrollbar(MotionEvent ev) {
754         final float offsetX = mContent.getX();
755         final float offsetY = mContent.getY();
756         WidgetsRecyclerView rv = getRecyclerView();
757 
758         ev.offsetLocation(-offsetX, -offsetY);
759         boolean isOnScrollBar = rv != null && rv.getScrollbar() != null && rv.isHitOnScrollBar(ev);
760         ev.offsetLocation(offsetX, offsetY);
761 
762         return isOnScrollBar;
763     }
764 
765     /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
766     @VisibleForTesting
getWidgetsView(BaseActivity launcher)767     public static WidgetsRecyclerView getWidgetsView(BaseActivity launcher) {
768         return launcher.findViewById(R.id.primary_widgets_list_view);
769     }
770 
771     @Override
addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)772     public void addHintCloseAnim(
773             float distanceToMove, Interpolator interpolator, PendingAnimation target) {
774         target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator);
775     }
776 
777     @Override
onCloseComplete()778     protected void onCloseComplete() {
779         super.onCloseComplete();
780         AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
781     }
782 
783     @Override
getHeaderViewHeight()784     public int getHeaderViewHeight() {
785         return measureHeightWithVerticalMargins(mHeaderTitle)
786                 + measureHeightWithVerticalMargins(mSearchBarContainer);
787     }
788 
789     /** private the height, in pixel, + the vertical margins of a given view. */
measureHeightWithVerticalMargins(View view)790     protected static int measureHeightWithVerticalMargins(View view) {
791         if (view == null || view.getVisibility() != VISIBLE) {
792             return 0;
793         }
794         MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
795         return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
796                 + marginLayoutParams.topMargin;
797     }
798 
getCurrentAdapterHolderType()799     private int getCurrentAdapterHolderType() {
800         if (mIsInSearchMode) {
801             return SEARCH;
802         } else if (mViewPager != null) {
803             return mViewPager.getCurrentPage();
804         } else {
805             return AdapterHolder.PRIMARY;
806         }
807     }
808 
restorePreviousAdapterHolderType(int previousAdapterHolderType)809     private void restorePreviousAdapterHolderType(int previousAdapterHolderType) {
810         if (previousAdapterHolderType == AdapterHolder.WORK && mViewPager != null) {
811             mViewPager.setCurrentPage(previousAdapterHolderType);
812         } else if (previousAdapterHolderType == AdapterHolder.SEARCH) {
813             enterSearchMode(false);
814         }
815     }
816 
817     @Override
onDeviceProfileChanged(DeviceProfile dp)818     public void onDeviceProfileChanged(DeviceProfile dp) {
819         super.onDeviceProfileChanged(dp);
820 
821         if (shouldRecreateLayout(/*oldDp=*/ mDeviceProfile, /*newDp=*/ dp)) {
822             SparseArray<Parcelable> widgetsState = new SparseArray<>();
823             saveHierarchyState(widgetsState);
824             handleClose(false);
825             WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
826             sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap);
827             sheet.restoreHierarchyState(widgetsState);
828             sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
829         } else if (!isTwoPane()) {
830             reset();
831             resetExpandedHeaders();
832         }
833 
834         mDeviceProfile = dp;
835     }
836 
restoreRecommendations(List<WidgetItem> recommendedWidgets, Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap)837     private void restoreRecommendations(List<WidgetItem> recommendedWidgets,
838             Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap) {
839         mRecommendedWidgets = recommendedWidgets;
840         mRecommendedWidgetsMap = recommendedWidgetsMap;
841     }
842 
843     /**
844      * Indicates if layout should be re-created on device profile change - so that a different
845      * layout can be displayed.
846      */
shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp)847     private static boolean shouldRecreateLayout(DeviceProfile oldDp, DeviceProfile newDp) {
848         // When folding/unfolding the foldables, we need to switch between the regular widget picker
849         // and the two pane picker, so we rebuild the picker with the correct layout.
850         boolean isFoldUnFold =
851                 oldDp.isTwoPanels != newDp.isTwoPanels && enableUnfoldedTwoPanePicker();
852         // In tablets, on orientation change we switch between single and two pane picker unless the
853         // categorized suggestions flag was on. With the categorized suggestions feature, we use a
854         // two pane picker across all orientations.
855         boolean useDifferentLayoutOnOrientationChange =
856                 (!enableCategorizedWidgetSuggestions() && (newDp.isTablet && !newDp.isTwoPanels
857                         && oldDp.isLandscape != newDp.isLandscape));
858 
859         return isFoldUnFold || useDifferentLayoutOnOrientationChange;
860     }
861 
862     /**
863      * In widget search mode, we should scale down content inside widget bottom sheet, rather
864      * than the whole bottom sheet, to indicate we will navigate back within the widget
865      * bottom sheet.
866      */
867     @Override
shouldAnimateContentViewInBackSwipe()868     public boolean shouldAnimateContentViewInBackSwipe() {
869         return mIsInSearchMode;
870     }
871 
872     @Override
onBackInvoked()873     public void onBackInvoked() {
874         if (mIsInSearchMode) {
875             mSearchBar.reset();
876             // Posting animation to next frame will let widget sheet finish updating UI first, and
877             // make animation smoother.
878             post(this::animateSwipeToDismissProgressToStart);
879         } else {
880             super.onBackInvoked();
881         }
882     }
883 
884     @Override
onDragStart(boolean start, float startDisplacement)885     public void onDragStart(boolean start, float startDisplacement) {
886         super.onDragStart(start, startDisplacement);
887         WindowInsetsController insetsController = getWindowInsetsController();
888         if (insetsController != null) {
889             insetsController.hide(WindowInsets.Type.ime());
890         }
891     }
892 
893     @Nullable
getViewToShowEducationTip()894     private View getViewToShowEducationTip() {
895         if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
896             return mWidgetRecommendationsView.getViewForEducationTip();
897         }
898 
899         AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
900                 ? AdapterHolder.SEARCH
901                 : mViewPager == null
902                         ? AdapterHolder.PRIMARY
903                         : mViewPager.getCurrentPage());
904         WidgetsRowViewHolder viewHolderForTip =
905                 (WidgetsRowViewHolder) IntStream.range(
906                                 0, adapterHolder.mWidgetsListAdapter.getItemCount())
907                         .mapToObj(adapterHolder.mWidgetsRecyclerView::
908                                 findViewHolderForAdapterPosition)
909                         .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder)
910                         .findFirst()
911                         .orElse(null);
912         if (viewHolderForTip != null) {
913             return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0);
914         }
915 
916         return null;
917     }
918 
isTwoPane()919     protected boolean isTwoPane() {
920         return false;
921     }
922 
923     /** Gets the sheet for widget picker, which is used for testing. */
924     @VisibleForTesting
getSheet()925     public View getSheet() {
926         return mContent;
927     }
928 
929     /** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */
930     @VisibleForTesting
openFirstHeader()931     public void openFirstHeader() {
932         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.selectFirstHeaderEntry();
933         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
934     }
935 
936     @Override
getHeaderTopClip(@onNull WidgetCell cell)937     protected int getHeaderTopClip(@NonNull WidgetCell cell) {
938         StickyHeaderLayout header = findViewById(R.id.search_and_recommendations_container);
939         if (header == null) {
940             return 0;
941         }
942         Rect cellRect = new Rect();
943         boolean cellIsPartiallyVisible = cell.getGlobalVisibleRect(cellRect);
944         if (cellIsPartiallyVisible) {
945             Rect occludingRect = new Rect();
946             for (View headerChild : header.getStickyChildren()) {
947                 Rect childRect = new Rect();
948                 boolean childVisible = headerChild.getGlobalVisibleRect(childRect);
949                 if (childVisible && childRect.intersect(cellRect)) {
950                     occludingRect.union(childRect);
951                 }
952             }
953             if (!occludingRect.isEmpty() && cellRect.top < occludingRect.bottom) {
954                 return occludingRect.bottom - cellRect.top;
955             }
956         }
957         return 0;
958     }
959 
960     @Override
scrollCellContainerByY(WidgetCell wc, int scrollByY)961     protected void scrollCellContainerByY(WidgetCell wc, int scrollByY) {
962         for (ViewParent parent = wc.getParent(); parent != null; parent = parent.getParent()) {
963             if (parent instanceof WidgetsRecyclerView recyclerView) {
964                 // Scrollable container for main widget list.
965                 recyclerView.smoothScrollBy(0, scrollByY);
966                 return;
967             } else if (parent instanceof StickyHeaderLayout header) {
968                 // Scrollable container for recommendations. We still scroll on the recycler (even
969                 // though the recommendations are not in the recycler view) because the
970                 // StickyHeaderLayout scroll is connected to the currently visible recycler view.
971                 WidgetsRecyclerView recyclerView = findVisibleRecyclerView();
972                 if (recyclerView != null) {
973                     recyclerView.smoothScrollBy(0, scrollByY);
974                 }
975                 return;
976             } else if (parent == this) {
977                 return;
978             }
979         }
980     }
981 
982     @Nullable
findVisibleRecyclerView()983     private WidgetsRecyclerView findVisibleRecyclerView() {
984         if (mViewPager != null) {
985             return (WidgetsRecyclerView) mViewPager.getPageAt(mViewPager.getCurrentPage());
986         }
987         return findViewById(R.id.primary_widgets_list_view);
988     }
989 
990     /** A holder class for holding adapters & their corresponding recycler view. */
991     final class AdapterHolder {
992         static final int PRIMARY = 0;
993         static final int WORK = 1;
994         static final int SEARCH = 2;
995 
996         private final int mAdapterType;
997         final WidgetsListAdapter mWidgetsListAdapter;
998         private final DefaultItemAnimator mWidgetsListItemAnimator;
999 
1000         WidgetsRecyclerView mWidgetsRecyclerView;
1001 
AdapterHolder(int adapterType)1002         AdapterHolder(int adapterType) {
1003             mAdapterType = adapterType;
1004             Context context = getContext();
1005 
1006             mWidgetsListAdapter = new WidgetsListAdapter(
1007                     context,
1008                     LayoutInflater.from(context),
1009                     this::getEmptySpaceHeight,
1010                     /* iconClickListener= */ WidgetsFullSheet.this,
1011                     /* iconLongClickListener= */ WidgetsFullSheet.this,
1012                     isTwoPane());
1013             mWidgetsListAdapter.setHasStableIds(true);
1014             switch (mAdapterType) {
1015                 case PRIMARY:
1016                     mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
1017                     break;
1018                 case WORK:
1019                     mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
1020                     break;
1021                 default:
1022                     break;
1023             }
1024             mWidgetsListItemAnimator = new WidgetsListItemAnimator();
1025         }
1026 
getEmptySpaceHeight()1027         private int getEmptySpaceHeight() {
1028             return mSearchScrollView.getHeaderHeight();
1029         }
1030 
setup(WidgetsRecyclerView recyclerView)1031         void setup(WidgetsRecyclerView recyclerView) {
1032             mWidgetsRecyclerView = recyclerView;
1033             mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider);
1034             mWidgetsRecyclerView.setClipToOutline(true);
1035             mWidgetsRecyclerView.setClipChildren(false);
1036             mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
1037             mWidgetsRecyclerView.bindFastScrollbar(mFastScroller);
1038             mWidgetsRecyclerView.setItemAnimator(isTwoPane() ? null : mWidgetsListItemAnimator);
1039             mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
1040             if (!isTwoPane()) {
1041                 mWidgetsRecyclerView.setEdgeEffectFactory(
1042                         ((SpringRelativeLayout) mContent).createEdgeEffectFactory());
1043             }
1044             // Recycler view binds to fast scroller when it is attached to screen. Make sure
1045             // search recycler view is bound to fast scroller if user is in search mode at the time
1046             // of attachment.
1047             if (mAdapterType == PRIMARY || mAdapterType == WORK) {
1048                 mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode);
1049             }
1050             mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(mMaxSpanPerRow);
1051         }
1052     }
1053 }
1054