• 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 android.view.View.MeasureSpec.makeMeasureSpec;
19 
20 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
21 import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
23 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
24 
25 import android.animation.Animator;
26 import android.content.Context;
27 import android.content.pm.LauncherApps;
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.graphics.Rect;
31 import android.os.Build;
32 import android.os.Process;
33 import android.os.UserHandle;
34 import android.os.UserManager;
35 import android.util.AttributeSet;
36 import android.util.Pair;
37 import android.util.SparseArray;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.WindowInsets;
43 import android.view.animation.AnimationUtils;
44 import android.view.animation.Interpolator;
45 import android.widget.Button;
46 import android.widget.TextView;
47 import android.window.BackEvent;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 import androidx.annotation.Px;
52 import androidx.annotation.RequiresApi;
53 import androidx.annotation.VisibleForTesting;
54 import androidx.recyclerview.widget.DefaultItemAnimator;
55 import androidx.recyclerview.widget.RecyclerView;
56 
57 import com.android.launcher3.DeviceProfile;
58 import com.android.launcher3.Launcher;
59 import com.android.launcher3.R;
60 import com.android.launcher3.Utilities;
61 import com.android.launcher3.anim.PendingAnimation;
62 import com.android.launcher3.compat.AccessibilityManagerCompat;
63 import com.android.launcher3.model.UserManagerState;
64 import com.android.launcher3.model.WidgetItem;
65 import com.android.launcher3.pm.UserCache;
66 import com.android.launcher3.views.ArrowTipView;
67 import com.android.launcher3.views.RecyclerViewFastScroller;
68 import com.android.launcher3.views.SpringRelativeLayout;
69 import com.android.launcher3.views.StickyHeaderLayout;
70 import com.android.launcher3.views.WidgetsEduView;
71 import com.android.launcher3.widget.BaseWidgetSheet;
72 import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
73 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
74 import com.android.launcher3.widget.picker.search.SearchModeListener;
75 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
76 import com.android.launcher3.widget.util.WidgetsTableUtils;
77 import com.android.launcher3.workprofile.PersonalWorkPagedView;
78 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
79 
80 import java.util.ArrayList;
81 import java.util.List;
82 import java.util.function.Predicate;
83 import java.util.stream.IntStream;
84 
85 /**
86  * Popup for showing the full list of available widgets
87  */
88 public class WidgetsFullSheet extends BaseWidgetSheet
89         implements ProviderChangedListener, OnActivePageChangedListener,
90         WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
91 
92     private static final long FADE_IN_DURATION = 150;
93     private static final long EDUCATION_TIP_DELAY_MS = 200;
94     private static final long EDUCATION_DIALOG_DELAY_MS = 500;
95     private static final float VERTICAL_START_POSITION = 0.3f;
96     // The widget recommendation table can easily take over the entire screen on devices with small
97     // resolution or landscape on phone. This ratio defines the max percentage of content area that
98     // the table can display.
99     private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
100     private static final String KEY_WIDGETS_EDUCATION_DIALOG_SEEN =
101             "launcher.widgets_education_dialog_seen";
102 
103     private final UserManagerState mUserManagerState = new UserManagerState();
104     private final UserHandle mCurrentUser = Process.myUserHandle();
105     private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter =
106             entry -> mCurrentUser.equals(entry.mPkgItem.user);
107     private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
108             entry -> !mCurrentUser.equals(entry.mPkgItem.user)
109                     && !mUserManagerState.isUserQuiet(entry.mPkgItem.user);
110     protected final boolean mHasWorkProfile;
111     protected boolean mHasRecommendedWidgets;
112     protected final SparseArray<AdapterHolder> mAdapters = new SparseArray();
113     @Nullable private ArrowTipView mLatestEducationalTip;
114     private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
115             new OnLayoutChangeListener() {
116                 @Override
117                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
118                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
119                     if (hasSeenEducationTip()) {
120                         removeOnLayoutChangeListener(this);
121                         return;
122                     }
123 
124                     // Widgets are loaded asynchronously, We are adding a delay because we only want
125                     // to show the tip when the widget preview has finished loading and rendering in
126                     // this view.
127                     removeCallbacks(mShowEducationTipTask);
128                     postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
129                 }
130             };
131 
132     private final Runnable mShowEducationTipTask = () -> {
133         if (hasSeenEducationTip()) {
134             removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
135             return;
136         }
137         mLatestEducationalTip = showEducationTipOnViewIfPossible(getViewToShowEducationTip());
138         if (mLatestEducationalTip != null) {
139             removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
140         }
141     };
142 
143     private final OnAttachStateChangeListener mBindScrollbarInSearchMode =
144             new OnAttachStateChangeListener() {
145                 @Override
146                 public void onViewAttachedToWindow(View view) {
147                     WidgetsRecyclerView searchRecyclerView =
148                             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
149                     if (mIsInSearchMode && searchRecyclerView != null) {
150                         searchRecyclerView.bindFastScrollbar(mFastScroller);
151                     }
152                 }
153 
154                 @Override
155                 public void onViewDetachedFromWindow(View view) {
156                 }
157             };
158 
159     @Px private final int mTabsHeight;
160 
161     @Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
162     @Nullable private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
163     @Nullable PersonalWorkPagedView mViewPager;
164     private boolean mIsInSearchMode;
165     private boolean mIsNoWidgetsViewNeeded;
166     @Px private int mMaxSpanPerRow;
167     private DeviceProfile mDeviceProfile;
168 
169     private int mOrientation;
170 
171     protected TextView mNoWidgetsView;
172     protected StickyHeaderLayout mSearchScrollView;
173     protected WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
174     protected View mTabBar;
175     protected View mSearchBarContainer;
176     protected WidgetsSearchBar mSearchBar;
177     protected TextView mHeaderTitle;
178     protected RecyclerViewFastScroller mFastScroller;
179 
WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr)180     public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
181         super(context, attrs, defStyleAttr);
182         mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
183         mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
184         mOrientation = context.getResources().getConfiguration().orientation;
185         mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
186         mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
187         mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
188 
189         Resources resources = getResources();
190         mTabsHeight = mHasWorkProfile
191                 ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
192                 : 0;
193 
194         mUserManagerState.init(UserCache.INSTANCE.get(context),
195                 context.getSystemService(UserManager.class));
196     }
197 
WidgetsFullSheet(Context context, AttributeSet attrs)198     public WidgetsFullSheet(Context context, AttributeSet attrs) {
199         this(context, attrs, 0);
200     }
201 
202     @Override
onFinishInflate()203     protected void onFinishInflate() {
204         super.onFinishInflate();
205 
206         mContent = findViewById(R.id.container);
207         setContentBackgroundWithParent(getContext().getDrawable(R.drawable.bg_widgets_full_sheet),
208                 mContent);
209         mContent.setOutlineProvider(mViewOutlineProvider);
210         mContent.setClipToOutline(true);
211         setupSheet();
212     }
213 
setupSheet()214     protected void setupSheet() {
215         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
216         int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
217                 : R.layout.widgets_full_sheet_recyclerview;
218         layoutInflater.inflate(contentLayoutRes, mContent, true);
219 
220         setupViews();
221 
222         mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
223         mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
224         mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
225         mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
226 
227         onRecommendedWidgetsBound();
228         onWidgetsBound();
229         setUpEducationViewsIfNeeded();
230     }
231 
setupViews()232     protected void setupViews() {
233         mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
234         mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
235         mNoWidgetsView = findViewById(R.id.no_widgets_text);
236         mFastScroller = findViewById(R.id.fast_scroller);
237         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
238         mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view));
239         mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view));
240         if (mHasWorkProfile) {
241             mViewPager = findViewById(R.id.widgets_view_pager);
242             mViewPager.setOutlineProvider(mViewOutlineProvider);
243             mViewPager.setClipToOutline(true);
244             mViewPager.setClipChildren(false);
245             mViewPager.initParentViews(this);
246             mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
247             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
248             findViewById(R.id.tab_personal)
249                     .setOnClickListener((View view) -> mViewPager.snapToPage(0));
250             findViewById(R.id.tab_work)
251                     .setOnClickListener((View view) -> mViewPager.snapToPage(1));
252             mAdapters.get(AdapterHolder.WORK).setup(findViewById(R.id.work_widgets_list_view));
253             setDeviceManagementResources();
254         } else {
255             mViewPager = null;
256         }
257 
258         mTabBar = mSearchScrollView.findViewById(R.id.tabs);
259         mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
260         mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
261 
262         mSearchBar.initialize(
263                 mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
264     }
265 
setDeviceManagementResources()266     private void setDeviceManagementResources() {
267         if (mActivityContext.getStringCache() != null) {
268             Button personalTab = findViewById(R.id.tab_personal);
269             personalTab.setText(mActivityContext.getStringCache().widgetsPersonalTab);
270 
271             Button workTab = findViewById(R.id.tab_work);
272             workTab.setText(mActivityContext.getStringCache().widgetsWorkTab);
273         }
274     }
275 
276     @Override
onActivePageChanged(int currentActivePage)277     public void onActivePageChanged(int currentActivePage) {
278         AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
279         WidgetsRecyclerView currentRecyclerView =
280                 mAdapters.get(currentActivePage).mWidgetsRecyclerView;
281 
282         updateRecyclerViewVisibility(currentAdapterHolder);
283         attachScrollbarToRecyclerView(currentRecyclerView);
284     }
285 
286     @Override
287     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
onBackProgressed(@onNull BackEvent backEvent)288     public void onBackProgressed(@NonNull BackEvent backEvent) {
289         super.onBackProgressed(backEvent);
290         mFastScroller.setVisibility(backEvent.getProgress() > 0 ? View.INVISIBLE : View.VISIBLE);
291     }
292 
attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView)293     private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
294         recyclerView.bindFastScrollbar(mFastScroller);
295         if (mCurrentWidgetsRecyclerView != recyclerView) {
296             // Only reset the scroll position & expanded apps if the currently shown recycler view
297             // has been updated.
298             reset();
299             resetExpandedHeaders();
300             mCurrentWidgetsRecyclerView = recyclerView;
301             mSearchScrollView.setCurrentRecyclerView(recyclerView);
302         }
303     }
304 
updateRecyclerViewVisibility(AdapterHolder adapterHolder)305     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
306         // The first item is always an empty space entry. Look for any more items.
307         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
308         adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
309 
310         if (adapterHolder.mAdapterType == AdapterHolder.SEARCH) {
311             mNoWidgetsView.setText(R.string.no_search_results);
312         } else if (adapterHolder.mAdapterType == AdapterHolder.WORK
313                 && mUserManagerState.isAnyProfileQuietModeEnabled()
314                 && mActivityContext.getStringCache() != null) {
315             mNoWidgetsView.setText(mActivityContext.getStringCache().workProfilePausedTitle);
316         } else {
317             mNoWidgetsView.setText(R.string.no_widgets_available);
318         }
319         mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE);
320     }
321 
reset()322     private void reset() {
323         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
324         if (mHasWorkProfile) {
325             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
326         }
327         mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
328         mSearchScrollView.reset(/* animate= */ true);
329     }
330 
331     @VisibleForTesting
getRecyclerView()332     public WidgetsRecyclerView getRecyclerView() {
333         if (mIsInSearchMode) {
334             return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
335         }
336         if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
337             return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
338         }
339         return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView;
340     }
341 
342     @Override
getAccessibilityTarget()343     protected Pair<View, String> getAccessibilityTarget() {
344         return Pair.create(getRecyclerView(), getContext().getString(
345                 mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
346     }
347 
348     @Override
onAttachedToWindow()349     protected void onAttachedToWindow() {
350         super.onAttachedToWindow();
351         mActivityContext.getAppWidgetHolder().addProviderChangeListener(this);
352         notifyWidgetProvidersChanged();
353         onRecommendedWidgetsBound();
354     }
355 
356     @Override
onDetachedFromWindow()357     protected void onDetachedFromWindow() {
358         super.onDetachedFromWindow();
359         mActivityContext.getAppWidgetHolder().removeProviderChangeListener(this);
360         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
361                 .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
362         if (mHasWorkProfile) {
363             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView
364                     .removeOnAttachStateChangeListener(mBindScrollbarInSearchMode);
365         }
366     }
367 
368     @Override
setInsets(Rect insets)369     public void setInsets(Rect insets) {
370         super.setInsets(insets);
371         int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight);
372         setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, bottomPadding);
373         setBottomPadding(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView, bottomPadding);
374         if (mHasWorkProfile) {
375             setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, bottomPadding);
376         }
377         ((MarginLayoutParams) mNoWidgetsView.getLayoutParams()).bottomMargin = bottomPadding;
378 
379         if (bottomPadding > 0) {
380             setupNavBarColor();
381         } else {
382             clearNavBarColor();
383         }
384 
385         requestLayout();
386     }
387 
setBottomPadding(RecyclerView recyclerView, int bottomPadding)388     private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) {
389         recyclerView.setPadding(
390                 recyclerView.getPaddingLeft(),
391                 recyclerView.getPaddingTop(),
392                 recyclerView.getPaddingRight(),
393                 bottomPadding);
394     }
395 
396     @Override
onContentHorizontalMarginChanged(int contentHorizontalMarginInPx)397     protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) {
398         setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx);
399         if (mViewPager == null) {
400             setContentViewChildHorizontalPadding(
401                     mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
402                     contentHorizontalMarginInPx);
403         } else {
404             setContentViewChildHorizontalPadding(
405                     mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
406                     contentHorizontalMarginInPx);
407             setContentViewChildHorizontalPadding(
408                     mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView,
409                     contentHorizontalMarginInPx);
410         }
411         setContentViewChildHorizontalPadding(
412                 mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView,
413                 contentHorizontalMarginInPx);
414     }
415 
setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx)416     private static void setContentViewChildHorizontalMargin(View view, int horizontalMarginInPx) {
417         ViewGroup.MarginLayoutParams layoutParams =
418                 (ViewGroup.MarginLayoutParams) view.getLayoutParams();
419         layoutParams.setMarginStart(horizontalMarginInPx);
420         layoutParams.setMarginEnd(horizontalMarginInPx);
421     }
422 
setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx)423     private static void setContentViewChildHorizontalPadding(View view, int horizontalPaddingInPx) {
424         view.setPadding(horizontalPaddingInPx, view.getPaddingTop(), horizontalPaddingInPx,
425                 view.getPaddingBottom());
426     }
427 
428     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)429     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
430         doMeasure(widthMeasureSpec, heightMeasureSpec);
431 
432         if (updateMaxSpansPerRow()) {
433             doMeasure(widthMeasureSpec, heightMeasureSpec);
434         }
435     }
436 
437     /** Returns {@code true} if the max spans have been updated. */
updateMaxSpansPerRow()438     private boolean updateMaxSpansPerRow() {
439         if (getMeasuredWidth() == 0) return false;
440 
441         @Px int maxHorizontalSpan = getContentView().getMeasuredWidth()
442                 - (2 * mContentHorizontalMargin);
443         if (mMaxSpanPerRow != maxHorizontalSpan) {
444             mMaxSpanPerRow = maxHorizontalSpan;
445             mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
446                     maxHorizontalSpan);
447             mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
448                     maxHorizontalSpan);
449             if (mHasWorkProfile) {
450                 mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(
451                         maxHorizontalSpan);
452             }
453             onRecommendedWidgetsBound();
454             return true;
455         }
456         return false;
457     }
458 
getContentView()459     protected View getContentView() {
460         return mHasWorkProfile
461                 ? mViewPager
462                 : mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
463     }
464 
465     @Override
onLayout(boolean changed, int l, int t, int r, int b)466     protected void onLayout(boolean changed, int l, int t, int r, int b) {
467         int width = r - l;
468         int height = b - t;
469 
470         // Content is laid out as center bottom aligned
471         int contentWidth = mContent.getMeasuredWidth();
472         int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
473         mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
474                 contentLeft + contentWidth, height);
475 
476         setTranslationShift(mTranslationShift);
477     }
478 
479     @Override
notifyWidgetProvidersChanged()480     public void notifyWidgetProvidersChanged() {
481         mActivityContext.refreshAndBindWidgetsForPackageUser(null);
482     }
483 
484     @Override
onWidgetsBound()485     public void onWidgetsBound() {
486         if (mIsInSearchMode) {
487             return;
488         }
489         List<WidgetsListBaseEntry> allWidgets =
490                 mActivityContext.getPopupDataProvider().getAllWidgets();
491 
492         AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
493         primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
494 
495         if (mHasWorkProfile) {
496             mViewPager.setVisibility(VISIBLE);
497             mTabBar.setVisibility(VISIBLE);
498             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
499             workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
500             onActivePageChanged(mViewPager.getCurrentPage());
501         } else {
502             onActivePageChanged(0);
503         }
504         // Update recommended widgets section so that it occupies appropriate space on screen to
505         // leave enough space for presence/absence of mNoWidgetsView.
506         boolean isNoWidgetsViewNeeded =
507                 !mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.hasVisibleEntries()
508                         || (mHasWorkProfile && mAdapters.get(AdapterHolder.WORK)
509                         .mWidgetsListAdapter.hasVisibleEntries());
510         if (mIsNoWidgetsViewNeeded != isNoWidgetsViewNeeded) {
511             mIsNoWidgetsViewNeeded = isNoWidgetsViewNeeded;
512             onRecommendedWidgetsBound();
513         }
514     }
515 
516     @Override
enterSearchMode()517     public void enterSearchMode() {
518         if (mIsInSearchMode) return;
519         setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
520         attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
521         mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_SEARCHED);
522     }
523 
524     @Override
exitSearchMode()525     public void exitSearchMode() {
526         if (!mIsInSearchMode) return;
527         onSearchResults(new ArrayList<>());
528         setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
529         if (mHasWorkProfile) {
530             mViewPager.snapToPage(AdapterHolder.PRIMARY);
531         }
532         attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
533     }
534 
535     @Override
onSearchResults(List<WidgetsListBaseEntry> entries)536     public void onSearchResults(List<WidgetsListBaseEntry> entries) {
537         mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
538         updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
539     }
540 
setViewVisibilityBasedOnSearch(boolean isInSearchMode)541     protected void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
542         mIsInSearchMode = isInSearchMode;
543         if (isInSearchMode) {
544             mRecommendedWidgetsTable.setVisibility(GONE);
545             if (mHasWorkProfile) {
546                 mViewPager.setVisibility(GONE);
547                 mTabBar.setVisibility(GONE);
548             } else {
549                 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE);
550             }
551             updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
552             // Hide no search results view to prevent it from flashing on enter search.
553             mNoWidgetsView.setVisibility(GONE);
554         } else {
555             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
556             // Visibility of recommended widgets, recycler views and headers are handled in methods
557             // below.
558             onRecommendedWidgetsBound();
559             onWidgetsBound();
560         }
561     }
562 
resetExpandedHeaders()563     protected void resetExpandedHeaders() {
564         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.resetExpandedHeader();
565         mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.resetExpandedHeader();
566     }
567 
568     @Override
onRecommendedWidgetsBound()569     public void onRecommendedWidgetsBound() {
570         if (mIsInSearchMode) {
571             return;
572         }
573         List<WidgetItem> recommendedWidgets =
574                 mActivityContext.getPopupDataProvider().getRecommendedWidgets();
575         mHasRecommendedWidgets = recommendedWidgets.size() > 0;
576         if (mHasRecommendedWidgets) {
577             float noWidgetsViewHeight = 0;
578             if (mIsNoWidgetsViewNeeded) {
579                 // Make sure recommended section leaves enough space for noWidgetsView.
580                 Rect noWidgetsViewTextBounds = new Rect();
581                 mNoWidgetsView.getPaint()
582                         .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
583                                 mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
584                 noWidgetsViewHeight = noWidgetsViewTextBounds.height();
585             }
586             if (!isTwoPane()) {
587                 doMeasure(
588                         makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
589                                 MeasureSpec.EXACTLY),
590                         makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
591                                 MeasureSpec.EXACTLY));
592             }
593             float maxTableHeight = getMaxTableHeight(noWidgetsViewHeight);
594 
595             List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
596                     WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering(
597                             recommendedWidgets,
598                             mActivityContext,
599                             mActivityContext.getDeviceProfile(),
600                             mMaxSpanPerRow,
601                             mWidgetCellHorizontalPadding);
602             mRecommendedWidgetsTable.setRecommendedWidgets(
603                     recommendedWidgetsInTable, maxTableHeight);
604         } else {
605             mRecommendedWidgetsTable.setVisibility(GONE);
606         }
607     }
608 
609     /** b/209579563: "Widgets" header should be focused first. */
610     @Override
getAccessibilityInitialFocusView()611     protected View getAccessibilityInitialFocusView() {
612         return mHeaderTitle;
613     }
614 
getMaxTableHeight(float noWidgetsViewHeight)615     protected float getMaxTableHeight(float noWidgetsViewHeight) {
616         return (mContent.getMeasuredHeight()
617                 - mTabsHeight - getHeaderViewHeight()
618                 - noWidgetsViewHeight)
619                 * RECOMMENDATION_TABLE_HEIGHT_RATIO;
620     }
621 
open(boolean animate)622     private void open(boolean animate) {
623         if (animate) {
624             if (getPopupContainer().getInsets().bottom > 0) {
625                 mContent.setAlpha(0);
626                 setTranslationShift(VERTICAL_START_POSITION);
627             }
628             setUpOpenAnimation(mActivityContext.getDeviceProfile().bottomSheetOpenDuration);
629             Animator animator = mOpenCloseAnimation.getAnimationPlayer();
630             animator.setInterpolator(AnimationUtils.loadInterpolator(
631                     getContext(), android.R.interpolator.linear_out_slow_in));
632             post(() -> {
633                 animator.setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
634                         .start();
635                 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
636             });
637         } else {
638             setTranslationShift(TRANSLATION_SHIFT_OPENED);
639             post(this::announceAccessibilityChanges);
640         }
641     }
642 
643     @Override
handleClose(boolean animate)644     protected void handleClose(boolean animate) {
645         handleClose(animate, mActivityContext.getDeviceProfile().bottomSheetCloseDuration);
646     }
647 
648     @Override
isOfType(int type)649     protected boolean isOfType(int type) {
650         return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
651     }
652 
653     @Override
onControllerInterceptTouchEvent(MotionEvent ev)654     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
655         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
656             mNoIntercept = shouldScroll(ev);
657             if (mSearchBar.isSearchBarFocused()
658                     && !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) {
659                 mSearchBar.clearSearchBarFocus();
660             }
661         }
662 
663         return super.onControllerInterceptTouchEvent(ev);
664     }
665 
shouldScroll(MotionEvent ev)666     protected boolean shouldScroll(MotionEvent ev) {
667         boolean intercept = false;
668         WidgetsRecyclerView recyclerView = getRecyclerView();
669         RecyclerViewFastScroller scroller = recyclerView.getScrollbar();
670         // Disable swipe down when recycler view is scrolling
671         if (scroller.getThumbOffsetY() >= 0 && getPopupContainer().isEventOverView(scroller, ev)) {
672             intercept = true;
673         } else if (getPopupContainer().isEventOverView(recyclerView, ev)) {
674             intercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer());
675         }
676         return intercept;
677     }
678 
679     /** Shows the {@link WidgetsFullSheet} on the launcher. */
show(Launcher launcher, boolean animate)680     public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
681         boolean isTwoPane = LARGE_SCREEN_WIDGET_PICKER.get()
682                 && launcher.getDeviceProfile().isTablet
683                 && launcher.getDeviceProfile().isLandscape
684                 && !launcher.getDeviceProfile().isTwoPanels;
685 
686         WidgetsFullSheet sheet;
687         if (isTwoPane) {
688             sheet = (WidgetsTwoPaneSheet) launcher.getLayoutInflater().inflate(
689                     R.layout.widgets_two_pane_sheet,
690                     launcher.getDragLayer(),
691                     false);
692         } else {
693             sheet = (WidgetsFullSheet) launcher.getLayoutInflater().inflate(
694                     R.layout.widgets_full_sheet,
695                     launcher.getDragLayer(),
696                     false);
697         }
698 
699         sheet.attachToContainer();
700         sheet.mIsOpen = true;
701         sheet.open(animate);
702         return sheet;
703     }
704 
705     @Override
onInterceptTouchEvent(MotionEvent ev)706     public boolean onInterceptTouchEvent(MotionEvent ev) {
707         return isTouchOnScrollbar(ev) || super.onInterceptTouchEvent(ev);
708     }
709 
710     @Override
onTouchEvent(MotionEvent ev)711     public boolean onTouchEvent(MotionEvent ev) {
712         return maybeHandleTouchEvent(ev) || super.onTouchEvent(ev);
713     }
714 
maybeHandleTouchEvent(MotionEvent ev)715     private boolean maybeHandleTouchEvent(MotionEvent ev) {
716         boolean isEventHandled = false;
717 
718         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
719             mCurrentTouchEventRecyclerView = isTouchOnScrollbar(ev) ? getRecyclerView() : null;
720         }
721 
722         if (mCurrentTouchEventRecyclerView != null) {
723             final float offsetX = mContent.getX();
724             final float offsetY = mContent.getY();
725             ev.offsetLocation(-offsetX, -offsetY);
726             isEventHandled = mCurrentTouchEventRecyclerView.dispatchTouchEvent(ev);
727             ev.offsetLocation(offsetX, offsetY);
728         }
729 
730         if (ev.getAction() == MotionEvent.ACTION_UP
731                 || ev.getAction() == MotionEvent.ACTION_CANCEL) {
732             mCurrentTouchEventRecyclerView = null;
733         }
734 
735         return isEventHandled;
736     }
737 
isTouchOnScrollbar(MotionEvent ev)738     private boolean isTouchOnScrollbar(MotionEvent ev) {
739         final float offsetX = mContent.getX();
740         final float offsetY = mContent.getY();
741         WidgetsRecyclerView rv = getRecyclerView();
742 
743         ev.offsetLocation(-offsetX, -offsetY);
744         boolean isOnScrollBar = rv != null && rv.getScrollbar() != null && rv.isHitOnScrollBar(ev);
745         ev.offsetLocation(offsetX, offsetY);
746 
747         return isOnScrollBar;
748     }
749 
750     /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
751     @VisibleForTesting
getWidgetsView(Launcher launcher)752     public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
753         return launcher.findViewById(R.id.primary_widgets_list_view);
754     }
755 
756     @Override
addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target)757     public void addHintCloseAnim(
758             float distanceToMove, Interpolator interpolator, PendingAnimation target) {
759         target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
760         target.setViewAlpha(getRecyclerView(), 0.5f, interpolator);
761     }
762 
763     @Override
onCloseComplete()764     protected void onCloseComplete() {
765         super.onCloseComplete();
766         removeCallbacks(mShowEducationTipTask);
767         if (mLatestEducationalTip != null) {
768             mLatestEducationalTip.close(true);
769         }
770         AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
771     }
772 
773     @Override
getHeaderViewHeight()774     public int getHeaderViewHeight() {
775         return measureHeightWithVerticalMargins(mHeaderTitle)
776                 + measureHeightWithVerticalMargins(mSearchBarContainer);
777     }
778 
779     /** private the height, in pixel, + the vertical margins of a given view. */
measureHeightWithVerticalMargins(View view)780     private static int measureHeightWithVerticalMargins(View view) {
781         if (view.getVisibility() != VISIBLE) {
782             return 0;
783         }
784         MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
785         return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
786                 + marginLayoutParams.topMargin;
787     }
788 
789     @Override
onConfigurationChanged(Configuration newConfig)790     protected void onConfigurationChanged(Configuration newConfig) {
791         super.onConfigurationChanged(newConfig);
792         if (mIsInSearchMode) {
793             mSearchBar.reset();
794         }
795 
796         // Checks the orientation of the screen
797         if (mOrientation != newConfig.orientation) {
798             mOrientation = newConfig.orientation;
799             if (LARGE_SCREEN_WIDGET_PICKER.get()
800                     && mDeviceProfile.isTablet && !mDeviceProfile.isTwoPanels) {
801                 handleClose(false);
802                 show(Launcher.getLauncher(getContext()), false);
803             } else {
804                 reset();
805             }
806         }
807     }
808 
809     @Override
onBackInvoked()810     public void onBackInvoked() {
811         if (mIsInSearchMode) {
812             mSearchBar.reset();
813             animateSlideInViewToNoScale();
814         } else {
815             super.onBackInvoked();
816         }
817     }
818 
819     @Override
onDragStart(boolean start, float startDisplacement)820     public void onDragStart(boolean start, float startDisplacement) {
821         super.onDragStart(start, startDisplacement);
822         getWindowInsetsController().hide(WindowInsets.Type.ime());
823     }
824 
getViewToShowEducationTip()825     @Nullable private View getViewToShowEducationTip() {
826         if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
827                 && mRecommendedWidgetsTable.getChildCount() > 0) {
828             return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
829         }
830 
831         AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
832                 ? AdapterHolder.SEARCH
833                 : mViewPager == null
834                         ? AdapterHolder.PRIMARY
835                         : mViewPager.getCurrentPage());
836         WidgetsRowViewHolder viewHolderForTip =
837                 (WidgetsRowViewHolder) IntStream.range(
838                                 0, adapterHolder.mWidgetsListAdapter.getItemCount())
839                         .mapToObj(adapterHolder.mWidgetsRecyclerView::
840                                 findViewHolderForAdapterPosition)
841                         .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder)
842                         .findFirst()
843                         .orElse(null);
844         if (viewHolderForTip != null) {
845             return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0);
846         }
847 
848         return null;
849     }
850 
851     /** Shows education dialog for widgets. */
showEducationDialog()852     private WidgetsEduView showEducationDialog() {
853         mActivityContext.getSharedPrefs().edit()
854                 .putBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, true).apply();
855         return WidgetsEduView.showEducationDialog(mActivityContext);
856     }
857 
858     /** Returns {@code true} if education dialog has previously been shown. */
hasSeenEducationDialog()859     protected boolean hasSeenEducationDialog() {
860         return mActivityContext.getSharedPrefs()
861                 .getBoolean(KEY_WIDGETS_EDUCATION_DIALOG_SEEN, false)
862                 || Utilities.isRunningInTestHarness();
863     }
864 
setUpEducationViewsIfNeeded()865     protected void setUpEducationViewsIfNeeded() {
866         if (!hasSeenEducationDialog()) {
867             postDelayed(() -> {
868                 WidgetsEduView eduDialog = showEducationDialog();
869                 eduDialog.addOnCloseListener(() -> {
870                     if (!hasSeenEducationTip()) {
871                         addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
872                         // Call #requestLayout() to trigger layout change listener in order to show
873                         // arrow tip immediately if there is a widget to show it on.
874                         requestLayout();
875                     }
876                 });
877             }, EDUCATION_DIALOG_DELAY_MS);
878         } else if (!hasSeenEducationTip()) {
879             addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
880         }
881     }
882 
isTwoPane()883     protected boolean isTwoPane() {
884         return false;
885     }
886 
887     /** Gets the sheet for widget picker, which is used for testing. */
888     @VisibleForTesting
getSheet()889     public View getSheet() {
890         return mContent;
891     }
892 
893     /** Opens the first header in widget picker and scrolls to the top of the RecyclerView. */
894     @VisibleForTesting
openFirstHeader()895     public void openFirstHeader() {
896         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.selectFirstHeaderEntry();
897         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
898     }
899 
900     /** A holder class for holding adapters & their corresponding recycler view. */
901     final class AdapterHolder {
902         static final int PRIMARY = 0;
903         static final int WORK = 1;
904         static final int SEARCH = 2;
905 
906         private final int mAdapterType;
907         final WidgetsListAdapter mWidgetsListAdapter;
908         private final DefaultItemAnimator mWidgetsListItemAnimator;
909 
910         WidgetsRecyclerView mWidgetsRecyclerView;
911 
AdapterHolder(int adapterType)912         AdapterHolder(int adapterType) {
913             mAdapterType = adapterType;
914             Context context = getContext();
915 
916             mWidgetsListAdapter = new WidgetsListAdapter(
917                     context,
918                     LayoutInflater.from(context),
919                     this::getEmptySpaceHeight,
920                     /* iconClickListener= */ WidgetsFullSheet.this,
921                     /* iconLongClickListener= */ WidgetsFullSheet.this,
922                     isTwoPane());
923             mWidgetsListAdapter.setHasStableIds(true);
924             switch (mAdapterType) {
925                 case PRIMARY:
926                     mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
927                     break;
928                 case WORK:
929                     mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
930                     break;
931                 default:
932                     break;
933             }
934             mWidgetsListItemAnimator = new DefaultItemAnimator();
935             // Disable change animations because it disrupts the item focus upon adapter item
936             // change.
937             mWidgetsListItemAnimator.setSupportsChangeAnimations(false);
938         }
939 
getEmptySpaceHeight()940         private int getEmptySpaceHeight() {
941             return mSearchScrollView.getHeaderHeight();
942         }
943 
setup(WidgetsRecyclerView recyclerView)944         void setup(WidgetsRecyclerView recyclerView) {
945             mWidgetsRecyclerView = recyclerView;
946             mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider);
947             mWidgetsRecyclerView.setClipToOutline(true);
948             mWidgetsRecyclerView.setClipChildren(false);
949             mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
950             mWidgetsRecyclerView.bindFastScrollbar(mFastScroller);
951             mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator);
952             mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
953             if (!isTwoPane()) {
954                 mWidgetsRecyclerView.setEdgeEffectFactory(
955                         ((SpringRelativeLayout) mContent).createEdgeEffectFactory());
956             }
957             // Recycler view binds to fast scroller when it is attached to screen. Make sure
958             // search recycler view is bound to fast scroller if user is in search mode at the time
959             // of attachment.
960             if (mAdapterType == PRIMARY || mAdapterType == WORK) {
961                 mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode);
962             }
963             mWidgetsListAdapter.setMaxHorizontalSpansPxPerRow(mMaxSpanPerRow);
964         }
965     }
966 }
967