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