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