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