• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.allapps;
17 
18 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
22 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
23 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.ValueAnimator;
28 import android.content.Context;
29 import android.graphics.Canvas;
30 import android.graphics.Color;
31 import android.graphics.Outline;
32 import android.graphics.Paint;
33 import android.graphics.Path;
34 import android.graphics.Path.Direction;
35 import android.graphics.Point;
36 import android.graphics.Rect;
37 import android.graphics.RectF;
38 import android.os.Bundle;
39 import android.os.Parcelable;
40 import android.os.Process;
41 import android.os.UserManager;
42 import android.util.AttributeSet;
43 import android.util.Log;
44 import android.util.SparseArray;
45 import android.util.TypedValue;
46 import android.view.KeyEvent;
47 import android.view.LayoutInflater;
48 import android.view.MotionEvent;
49 import android.view.View;
50 import android.view.ViewOutlineProvider;
51 import android.view.WindowInsets;
52 import android.widget.Button;
53 import android.widget.RelativeLayout;
54 
55 import androidx.annotation.NonNull;
56 import androidx.annotation.Nullable;
57 import androidx.annotation.Px;
58 import androidx.annotation.VisibleForTesting;
59 import androidx.core.graphics.ColorUtils;
60 import androidx.recyclerview.widget.RecyclerView;
61 
62 import com.android.launcher3.DeviceProfile;
63 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
64 import com.android.launcher3.DragSource;
65 import com.android.launcher3.DropTarget.DragObject;
66 import com.android.launcher3.Insettable;
67 import com.android.launcher3.InsettableFrameLayout;
68 import com.android.launcher3.R;
69 import com.android.launcher3.Utilities;
70 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
71 import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider;
72 import com.android.launcher3.allapps.search.SearchAdapterProvider;
73 import com.android.launcher3.config.FeatureFlags;
74 import com.android.launcher3.keyboard.FocusedItemDecorator;
75 import com.android.launcher3.model.StringCache;
76 import com.android.launcher3.model.data.ItemInfo;
77 import com.android.launcher3.testing.shared.TestProtocol;
78 import com.android.launcher3.util.ItemInfoMatcher;
79 import com.android.launcher3.util.Themes;
80 import com.android.launcher3.views.ActivityContext;
81 import com.android.launcher3.views.BaseDragLayer;
82 import com.android.launcher3.views.RecyclerViewFastScroller;
83 import com.android.launcher3.views.ScrimView;
84 import com.android.launcher3.views.SpringRelativeLayout;
85 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip;
86 
87 import java.util.ArrayList;
88 import java.util.Arrays;
89 import java.util.List;
90 import java.util.function.Predicate;
91 import java.util.stream.Stream;
92 
93 /**
94  * All apps container view with search support for use in a dragging activity.
95  *
96  * @param <T> Type of context inflating all apps.
97  */
98 public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
99         extends SpringRelativeLayout implements DragSource, Insettable,
100         OnDeviceProfileChangeListener, PersonalWorkSlidingTabStrip.OnActivePageChangedListener,
101         ScrimView.ScrimDrawingController {
102 
103     public static final float PULL_MULTIPLIER = .02f;
104     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
105     protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
106     private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300;
107     // Render the header protection at all times to debug clipping issues.
108     private static final boolean DEBUG_HEADER_PROTECTION = false;
109     /** Context of an activity or window that is inflating this container. */
110 
111     protected final T mActivityContext;
112     protected final List<AdapterHolder> mAH;
113     protected final Predicate<ItemInfo> mPersonalMatcher = ItemInfoMatcher.ofUser(
114             Process.myUserHandle());
115     protected final WorkProfileManager mWorkManager;
116     protected final Point mFastScrollerOffset = new Point();
117     protected final int mScrimColor;
118     protected final float mHeaderThreshold;
119 
120     // Used to animate Search results out and A-Z apps in, or vice-versa.
121     private final SearchTransitionController mSearchTransitionController;
122     private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
123     private final Rect mInsets = new Rect();
124     private final AllAppsStore mAllAppsStore = new AllAppsStore();
125     private final RecyclerView.OnScrollListener mScrollListener =
126             new RecyclerView.OnScrollListener() {
127                 @Override
128                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
129                     updateHeaderScroll(recyclerView.computeVerticalScrollOffset());
130                 }
131             };
132     private final Paint mNavBarScrimPaint;
133     private final int mHeaderProtectionColor;
134     private final Path mTmpPath = new Path();
135     private final RectF mTmpRectF = new RectF();
136     protected AllAppsPagedView mViewPager;
137     protected FloatingHeaderView mHeader;
138     protected View mBottomSheetBackground;
139     protected RecyclerViewFastScroller mFastScroller;
140 
141     /**
142      * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}.
143      */
144     protected View mSearchContainer;
145     protected SearchUiManager mSearchUiManager;
146     protected boolean mUsingTabs;
147     protected RecyclerViewFastScroller mTouchHandler;
148 
149     /** {@code true} when rendered view is in search state instead of the scroll state. */
150     private boolean mIsSearching;
151     private boolean mRebindAdaptersAfterSearchAnimation;
152     private int mNavBarScrimHeight = 0;
153     private SearchRecyclerView mSearchRecyclerView;
154     protected SearchAdapterProvider<?> mMainAdapterProvider;
155     private View mBottomSheetHandleArea;
156     private boolean mHasWorkApps;
157     private float[] mBottomSheetCornerRadii;
158     private ScrimView mScrimView;
159     private int mHeaderColor;
160     private int mBottomSheetBackgroundColor;
161     private int mTabsProtectionAlpha;
162     @Nullable private AllAppsTransitionController mAllAppsTransitionController;
163 
ActivityAllAppsContainerView(Context context)164     public ActivityAllAppsContainerView(Context context) {
165         this(context, null);
166     }
167 
ActivityAllAppsContainerView(Context context, AttributeSet attrs)168     public ActivityAllAppsContainerView(Context context, AttributeSet attrs) {
169         this(context, attrs, 0);
170     }
171 
ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr)172     public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
173         super(context, attrs, defStyleAttr);
174         mActivityContext = ActivityContext.lookupContext(context);
175 
176         mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
177         mHeaderThreshold = getResources().getDimensionPixelSize(
178                 R.dimen.dynamic_grid_cell_border_spacing);
179         mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor);
180 
181         mWorkManager = new WorkProfileManager(
182                 mActivityContext.getSystemService(UserManager.class),
183                 this, mActivityContext.getStatsLogManager());
184         mAH = Arrays.asList(null, null, null);
185         mNavBarScrimPaint = new Paint();
186         mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor));
187 
188         AllAppsStore.OnUpdateListener onAppsUpdated = this::onAppsUpdated;
189         if (TestProtocol.sDebugTracing) {
190             Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainer#init registeringListener: " +
191                     onAppsUpdated);
192         }
193         mAllAppsStore.addUpdateListener(onAppsUpdated);
194 
195         // This is a focus listener that proxies focus from a view into the list view.  This is to
196         // work around the search box from getting first focus and showing the cursor.
197         setOnFocusChangeListener((v, hasFocus) -> {
198             if (hasFocus && getActiveRecyclerView() != null) {
199                 getActiveRecyclerView().requestFocus();
200             }
201         });
202         initContent();
203 
204         mSearchTransitionController = new SearchTransitionController(this);
205     }
206 
207     /**
208      * Initializes the view hierarchy and internal variables. Any initialization which actually uses
209      * these members should be done in {@link #onFinishInflate()}.
210      * In terms of subclass initialization, the following would be parallel order for activity:
211      *   initContent -> onPreCreate
212      *   constructor/init -> onCreate
213      *   onFinishInflate -> onPostCreate
214      */
initContent()215     protected void initContent() {
216         mMainAdapterProvider = createMainAdapterProvider();
217 
218         mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN,
219                 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, null)));
220         mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK,
221                 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, mWorkManager)));
222         mAH.set(SEARCH, new AdapterHolder(SEARCH,
223                 new AlphabeticalAppsList<>(mActivityContext, null, null)));
224 
225         getLayoutInflater().inflate(R.layout.all_apps_content, this);
226         mHeader = findViewById(R.id.all_apps_header);
227         mBottomSheetBackground = findViewById(R.id.bottom_sheet_background);
228         mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area);
229         mSearchRecyclerView = findViewById(R.id.search_results_list_view);
230         mFastScroller = findViewById(R.id.fast_scroller);
231         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
232 
233         // Add the search box above everything else.
234         mSearchContainer = inflateSearchBox();
235         addView(mSearchContainer);
236         mSearchUiManager = (SearchUiManager) mSearchContainer;
237     }
238 
239     @Override
onFinishInflate()240     protected void onFinishInflate() {
241         super.onFinishInflate();
242 
243         mAH.get(SEARCH).setup(mSearchRecyclerView,
244                 /* Filter out A-Z apps */ itemInfo -> false);
245         rebindAdapters(true /* force */);
246         float cornerRadius = Themes.getDialogCornerRadius(getContext());
247         mBottomSheetCornerRadii = new float[]{
248                 cornerRadius,
249                 cornerRadius, // Top left radius in px
250                 cornerRadius,
251                 cornerRadius, // Top right radius in px
252                 0,
253                 0, // Bottom right
254                 0,
255                 0 // Bottom left
256         };
257         final TypedValue value = new TypedValue();
258         getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true);
259         mBottomSheetBackgroundColor = value.data;
260         updateBackground(mActivityContext.getDeviceProfile());
261         mSearchUiManager.initializeSearch(this);
262     }
263 
264     @Override
onAttachedToWindow()265     protected void onAttachedToWindow() {
266         super.onAttachedToWindow();
267         mActivityContext.addOnDeviceProfileChangeListener(this);
268     }
269 
270     @Override
onDetachedFromWindow()271     protected void onDetachedFromWindow() {
272         super.onDetachedFromWindow();
273         mActivityContext.removeOnDeviceProfileChangeListener(this);
274     }
275 
getSearchUiManager()276     public SearchUiManager getSearchUiManager() {
277         return mSearchUiManager;
278     }
279 
getSearchView()280     public View getSearchView() {
281         return mSearchContainer;
282     }
283 
284     /** Invoke when the current search session is finished. */
onClearSearchResult()285     public void onClearSearchResult() {
286         getMainAdapterProvider().clearHighlightedItem();
287         animateToSearchState(false);
288         rebindAdapters();
289     }
290 
291     /**
292      * Sets results list for search
293      */
setSearchResults(ArrayList<AdapterItem> results)294     public void setSearchResults(ArrayList<AdapterItem> results) {
295         getMainAdapterProvider().clearHighlightedItem();
296         if (getSearchResultList().setSearchResults(results)) {
297             getSearchRecyclerView().onSearchResultsChanged();
298         }
299         if (results != null) {
300             animateToSearchState(true);
301         }
302     }
303 
304     /**
305      * Sets results list for search.
306      *
307      * @param searchResultCode indicates if the result is final or intermediate for a given query
308      *                         since we can get search results from multiple sources.
309      */
setSearchResults(ArrayList<AdapterItem> results, int searchResultCode)310     public void setSearchResults(ArrayList<AdapterItem> results, int searchResultCode) {
311         setSearchResults(results);
312     }
313 
animateToSearchState(boolean goingToSearch)314     private void animateToSearchState(boolean goingToSearch) {
315         animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS);
316     }
317 
setAllAppsTransitionController( AllAppsTransitionController allAppsTransitionController)318     public void setAllAppsTransitionController(
319             AllAppsTransitionController allAppsTransitionController) {
320         mAllAppsTransitionController = allAppsTransitionController;
321     }
322 
animateToSearchState(boolean goingToSearch, long durationMs)323     private void animateToSearchState(boolean goingToSearch, long durationMs) {
324         if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) {
325             return;
326         }
327         mFastScroller.setVisibility(goingToSearch ? INVISIBLE : VISIBLE);
328         if (goingToSearch) {
329             // Fade out the button to pause work apps.
330             mWorkManager.onActivePageChanged(SEARCH);
331         } else if (mAllAppsTransitionController != null) {
332             // If exiting search, revert predictive back scale on all apps
333             mAllAppsTransitionController.animateAllAppsToNoScale();
334         }
335         mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
336                 /* onEndRunnable = */ () -> {
337                     mIsSearching = goingToSearch;
338                     updateSearchResultsVisibility();
339                     int previousPage = getCurrentPage();
340                     if (mRebindAdaptersAfterSearchAnimation) {
341                         rebindAdapters(false);
342                         mRebindAdaptersAfterSearchAnimation = false;
343                     }
344                     if (!goingToSearch) {
345                         setSearchResults(null);
346                         if (mViewPager != null) {
347                             mViewPager.setCurrentPage(previousPage);
348                         }
349                         onActivePageChanged(previousPage);
350                     }
351                 });
352     }
353 
shouldContainerScroll(MotionEvent ev)354     public boolean shouldContainerScroll(MotionEvent ev) {
355         BaseDragLayer dragLayer = mActivityContext.getDragLayer();
356         // IF the MotionEvent is inside the search box or handle area, and the container keeps on
357         // receiving touch input, container should move down.
358         if (dragLayer.isEventOverView(mSearchContainer, ev)
359                 || dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) {
360             return true;
361         }
362         AllAppsRecyclerView rv = getActiveRecyclerView();
363         if (rv == null) {
364             return true;
365         }
366         if (rv.getScrollbar() != null
367                 && rv.getScrollbar().getThumbOffsetY() >= 0
368                 && dragLayer.isEventOverView(rv.getScrollbar(), ev)) {
369             return false;
370         }
371         // Scroll if not within the container view (e.g. over large-screen scrim).
372         if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) {
373             return true;
374         }
375         return rv.shouldContainerScroll(ev, dragLayer);
376     }
377 
reset(boolean animate)378     public void reset(boolean animate) {
379         for (int i = 0; i < mAH.size(); i++) {
380             if (mAH.get(i).mRecyclerView != null) {
381                 mAH.get(i).mRecyclerView.scrollToTop();
382             }
383         }
384         if (mTouchHandler != null) {
385             mTouchHandler.endFastScrolling();
386         }
387         if (mHeader != null && mHeader.getVisibility() == VISIBLE) {
388             mHeader.reset(animate);
389         }
390         // Reset the base recycler view after transitioning home.
391         updateHeaderScroll(0);
392         // Reset the search bar after transitioning home.
393         mSearchUiManager.resetSearch();
394         // Animate to A-Z with 0 time to reset the animation with proper state management.
395         animateToSearchState(false, 0);
396     }
397 
398     @Override
dispatchKeyEvent(KeyEvent event)399     public boolean dispatchKeyEvent(KeyEvent event) {
400         mSearchUiManager.preDispatchKeyEvent(event);
401         return super.dispatchKeyEvent(event);
402     }
403 
getDescription()404     public String getDescription() {
405         if (!mUsingTabs && isSearching()) {
406             return getContext().getString(R.string.all_apps_search_results);
407         } else {
408             StringCache cache = mActivityContext.getStringCache();
409             if (mUsingTabs) {
410                 if (cache != null) {
411                     return isPersonalTab()
412                             ? cache.allAppsPersonalTabAccessibility
413                             : cache.allAppsWorkTabAccessibility;
414                 } else {
415                     return isPersonalTab()
416                             ? getContext().getString(R.string.all_apps_button_personal_label)
417                             : getContext().getString(R.string.all_apps_button_work_label);
418                 }
419             }
420             return getContext().getString(R.string.all_apps_button_label);
421         }
422     }
423 
isSearching()424     public boolean isSearching() {
425         return mIsSearching;
426     }
427 
428     @Override
onActivePageChanged(int currentActivePage)429     public void onActivePageChanged(int currentActivePage) {
430         if (mSearchTransitionController.isRunning()) {
431             // Will be called at the end of the animation.
432             return;
433         }
434         if (mAH.get(currentActivePage).mRecyclerView != null) {
435             mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller);
436         }
437         // Header keeps track of active recycler view to properly render header protection.
438         mHeader.setActiveRV(currentActivePage);
439         reset(true /* animate */);
440 
441         mWorkManager.onActivePageChanged(currentActivePage);
442     }
443 
rebindAdapters()444     protected void rebindAdapters() {
445         rebindAdapters(false /* force */);
446     }
447 
rebindAdapters(boolean force)448     protected void rebindAdapters(boolean force) {
449         if (mSearchTransitionController.isRunning()) {
450             mRebindAdaptersAfterSearchAnimation = true;
451             return;
452         }
453         updateSearchResultsVisibility();
454 
455         boolean showTabs = shouldShowTabs();
456         if (showTabs == mUsingTabs && !force) {
457             return;
458         }
459 
460         if (isSearching()) {
461             mUsingTabs = showTabs;
462             mWorkManager.detachWorkModeSwitch();
463             return;
464         }
465 
466         if (!FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
467             RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator();
468             getSearchRecyclerView().removeItemDecoration(decoration);
469             getSearchRecyclerView().addItemDecoration(decoration);
470         }
471 
472         // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND
473         // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen
474         // after this call.
475         replaceAppsRVContainer(showTabs);
476         mUsingTabs = showTabs;
477 
478         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView);
479         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
480         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
481 
482         if (mUsingTabs) {
483             mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher);
484             mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
485             mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work);
486             if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
487                 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
488                         mWorkManager.newScrollListener());
489             }
490             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
491             findViewById(R.id.tab_personal)
492                     .setOnClickListener((View view) -> {
493                         if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
494                             mActivityContext.getStatsLogManager().logger()
495                                     .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
496                         }
497                         mActivityContext.hideKeyboard();
498                     });
499             findViewById(R.id.tab_work)
500                     .setOnClickListener((View view) -> {
501                         if (mViewPager.snapToPage(AdapterHolder.WORK)) {
502                             mActivityContext.getStatsLogManager().logger()
503                                     .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
504                         }
505                         mActivityContext.hideKeyboard();
506                     });
507             setDeviceManagementResources();
508             onActivePageChanged(mViewPager.getNextPage());
509         } else {
510             mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
511             mAH.get(AdapterHolder.WORK).mRecyclerView = null;
512         }
513         setupHeader();
514 
515         if (isSearchBarOnBottom()) {
516             // Keep the scroller above the search bar.
517             RelativeLayout.LayoutParams scrollerLayoutParams =
518                     (LayoutParams) mFastScroller.getLayoutParams();
519             scrollerLayoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps);
520             scrollerLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
521             scrollerLayoutParams.bottomMargin = getResources().getDimensionPixelSize(
522                     R.dimen.fastscroll_bottom_margin_floating_search);
523         }
524 
525         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView);
526         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
527         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
528     }
529 
replaceAppsRVContainer(boolean showTabs)530     protected View replaceAppsRVContainer(boolean showTabs) {
531         for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) {
532             AdapterHolder adapterHolder = mAH.get(i);
533             if (adapterHolder.mRecyclerView != null) {
534                 adapterHolder.mRecyclerView.setLayoutManager(null);
535                 adapterHolder.mRecyclerView.setAdapter(null);
536             }
537         }
538         View oldView = getAppsRecyclerViewContainer();
539         int index = indexOfChild(oldView);
540         removeView(oldView);
541         int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
542         final View rvContainer = getLayoutInflater().inflate(layout, this, false);
543         addView(rvContainer, index);
544         if (showTabs) {
545             mViewPager = (AllAppsPagedView) rvContainer;
546             mViewPager.initParentViews(this);
547             mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
548             mViewPager.setOutlineProvider(new ViewOutlineProvider() {
549                 @Override
550                 public void getOutline(View view, Outline outline) {
551                     @Px final int bottomOffsetPx =
552                             (int) (ActivityAllAppsContainerView.this.getMeasuredHeight()
553                                     * PREDICTIVE_BACK_MIN_SCALE);
554                     outline.setRect(
555                             0,
556                             0,
557                             view.getMeasuredWidth(),
558                             view.getMeasuredHeight() + bottomOffsetPx);
559                 }
560             });
561 
562             mWorkManager.reset();
563             post(() -> mAH.get(AdapterHolder.WORK).applyPadding());
564 
565         } else {
566             mWorkManager.detachWorkModeSwitch();
567             mViewPager = null;
568         }
569 
570         removeCustomRules(rvContainer);
571         removeCustomRules(getSearchRecyclerView());
572         if (!isSearchSupported()) {
573             layoutWithoutSearchContainer(rvContainer, showTabs);
574         } else if (isSearchBarOnBottom()) {
575             alignParentTop(rvContainer, showTabs);
576             alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
577             layoutAboveSearchContainer(rvContainer);
578             layoutAboveSearchContainer(getSearchRecyclerView());
579         } else {
580             layoutBelowSearchContainer(rvContainer, showTabs);
581             layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false);
582         }
583 
584         return rvContainer;
585     }
586 
setupHeader()587     void setupHeader() {
588         mHeader.setVisibility(View.VISIBLE);
589         boolean tabsHidden = !mUsingTabs;
590         mHeader.setup(
591                 mAH.get(AdapterHolder.MAIN).mRecyclerView,
592                 mAH.get(AdapterHolder.WORK).mRecyclerView,
593                 (SearchRecyclerView) mAH.get(SEARCH).mRecyclerView,
594                 getCurrentPage(),
595                 tabsHidden);
596 
597         int padding = mHeader.getMaxTranslation();
598         mAH.forEach(adapterHolder -> {
599             adapterHolder.mPadding.top = padding;
600             adapterHolder.applyPadding();
601             if (adapterHolder.mRecyclerView != null) {
602                 adapterHolder.mRecyclerView.scrollToTop();
603             }
604         });
605 
606         removeCustomRules(mHeader);
607         if (!isSearchSupported()) {
608             layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
609         } else if (isSearchBarOnBottom()) {
610             alignParentTop(mHeader, false /* includeTabsMargin */);
611         } else {
612             layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */);
613         }
614     }
615 
updateHeaderScroll(int scrolledOffset)616     protected void updateHeaderScroll(int scrolledOffset) {
617         float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
618         int headerColor = getHeaderColor(prog1);
619         int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0
620                 : (int) (Utilities.boundToRange(
621                         (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f)
622                         * 255);
623         if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) {
624             mHeaderColor = headerColor;
625             mTabsProtectionAlpha = tabsAlpha;
626             invalidateHeader();
627         }
628         if (mSearchUiManager.getEditText() == null) {
629             return;
630         }
631 
632         float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
633         boolean bgVisible = mSearchUiManager.getBackgroundVisibility();
634         if (scrolledOffset == 0 && !isSearching()) {
635             bgVisible = true;
636         } else if (scrolledOffset > mHeaderThreshold) {
637             bgVisible = false;
638         }
639         mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog);
640     }
641 
getHeaderColor(float blendRatio)642     protected int getHeaderColor(float blendRatio) {
643         return ColorUtils.setAlphaComponent(
644                 ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio),
645                 (int) (mSearchContainer.getAlpha() * 255));
646     }
647 
648     /**
649      * It is up to the search container view created by {@link #inflateSearchBox()} to use the
650      * floating search bar flag to move itself to the bottom of this container. This method checks
651      * if that had been done; otherwise the flag will be ignored.
652      *
653      * @return true if the search bar is at the bottom of the container (as opposed to the top).
654      **/
isSearchBarOnBottom()655     private boolean isSearchBarOnBottom() {
656         return FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()
657                 && ((RelativeLayout.LayoutParams) mSearchContainer.getLayoutParams()).getRule(
658                 ALIGN_PARENT_BOTTOM) == RelativeLayout.TRUE;
659     }
660 
layoutBelowSearchContainer(View v, boolean includeTabsMargin)661     private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) {
662         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
663             return;
664         }
665 
666         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
667         layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.search_container_all_apps);
668 
669         int topMargin = getContext().getResources().getDimensionPixelSize(
670                 R.dimen.all_apps_header_top_margin);
671         if (includeTabsMargin) {
672             topMargin += getContext().getResources().getDimensionPixelSize(
673                     R.dimen.all_apps_header_pill_height);
674         }
675         layoutParams.topMargin = topMargin;
676     }
677 
layoutAboveSearchContainer(View v)678     private void layoutAboveSearchContainer(View v) {
679         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
680             return;
681         }
682 
683         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
684         layoutParams.addRule(RelativeLayout.ABOVE, R.id.search_container_all_apps);
685     }
686 
alignParentTop(View v, boolean includeTabsMargin)687     private void alignParentTop(View v, boolean includeTabsMargin) {
688         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
689             return;
690         }
691 
692         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
693         layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
694         layoutParams.topMargin =
695                 includeTabsMargin
696                         ? getContext().getResources().getDimensionPixelSize(
697                         R.dimen.all_apps_header_pill_height)
698                         : 0;
699     }
700 
removeCustomRules(View v)701     private void removeCustomRules(View v) {
702         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
703             return;
704         }
705 
706         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
707         layoutParams.removeRule(RelativeLayout.ABOVE);
708         layoutParams.removeRule(RelativeLayout.ALIGN_TOP);
709         layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP);
710     }
711 
createAdapter(AlphabeticalAppsList<T> appsList)712     protected BaseAllAppsAdapter<T> createAdapter(AlphabeticalAppsList<T> appsList) {
713         return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList,
714                 mMainAdapterProvider);
715     }
716 
717     // TODO(b/216683257): Remove when Taskbar All Apps supports search.
isSearchSupported()718     protected boolean isSearchSupported() {
719         return true;
720     }
721 
layoutWithoutSearchContainer(View v, boolean includeTabsMargin)722     private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) {
723         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
724             return;
725         }
726 
727         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
728         layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
729         layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin
730                 ? R.dimen.all_apps_header_pill_height
731                 : R.dimen.all_apps_header_top_margin);
732     }
733 
isInAllApps()734     public boolean isInAllApps() {
735         // TODO: Make this abstract
736         return true;
737     }
738 
739     /**
740      * Inflates the search box
741      */
inflateSearchBox()742     protected View inflateSearchBox() {
743         return getLayoutInflater().inflate(R.layout.search_container_all_apps, this, false);
744     }
745 
746     /** Creates the adapter provider for the main section. */
createMainAdapterProvider()747     protected SearchAdapterProvider<?> createMainAdapterProvider() {
748         return new DefaultSearchAdapterProvider(mActivityContext);
749     }
750 
751     /** The adapter provider for the main section. */
getMainAdapterProvider()752     public final SearchAdapterProvider<?> getMainAdapterProvider() {
753         return mMainAdapterProvider;
754     }
755 
756     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray)757     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) {
758         try {
759             // Many slice view id is not properly assigned, and hence throws null
760             // pointer exception in the underneath method. Catching the exception
761             // simply doesn't restore these slice views. This doesn't have any
762             // user visible effect because because we query them again.
763             super.dispatchRestoreInstanceState(sparseArray);
764         } catch (Exception e) {
765             Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e);
766         }
767 
768         Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null);
769         if (state != null) {
770             int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0);
771             if (currentPage == AdapterHolder.WORK && mViewPager != null) {
772                 mViewPager.setCurrentPage(currentPage);
773                 rebindAdapters();
774             } else {
775                 reset(true);
776             }
777         }
778     }
779 
780     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)781     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
782         super.dispatchSaveInstanceState(container);
783         Bundle state = new Bundle();
784         state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage());
785         container.put(R.id.work_tab_state_id, state);
786     }
787 
788     /**
789      * Sets the long click listener for icons
790      */
setOnIconLongClickListener(OnLongClickListener listener)791     public void setOnIconLongClickListener(OnLongClickListener listener) {
792         for (AdapterHolder holder : mAH) {
793             holder.mAdapter.setOnIconLongClickListener(listener);
794         }
795     }
796 
getAppsStore()797     public AllAppsStore getAppsStore() {
798         return mAllAppsStore;
799     }
800 
getWorkManager()801     public WorkProfileManager getWorkManager() {
802         return mWorkManager;
803     }
804 
805     @Override
onDeviceProfileChanged(DeviceProfile dp)806     public void onDeviceProfileChanged(DeviceProfile dp) {
807         for (AdapterHolder holder : mAH) {
808             holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns);
809             if (holder.mRecyclerView != null) {
810                 // Remove all views and clear the pool, while keeping the data same. After this
811                 // call, all the viewHolders will be recreated.
812                 holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true);
813                 holder.mRecyclerView.getRecycledViewPool().clear();
814             }
815         }
816         updateBackground(dp);
817     }
818 
updateBackground(DeviceProfile deviceProfile)819     protected void updateBackground(DeviceProfile deviceProfile) {
820         mBottomSheetBackground.setVisibility(deviceProfile.isTablet ? View.VISIBLE : View.GONE);
821         // Note: For tablets, the opaque background and header protection are added in drawOnScrim.
822         // For the taskbar entrypoint, the scrim is drawn differently, so a static background is
823         // added in TaskbarAllAppsContainerView and header protection is not yet supported.
824     }
825 
onAppsUpdated()826     private void onAppsUpdated() {
827         mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher());
828         if (TestProtocol.sDebugTracing) {
829             Log.d(WORK_TAB_MISSING, "ActivityAllAppsContainerView#onAppsUpdated hasWorkApps: " +
830                     mHasWorkApps + " allApps: " + mAllAppsStore.getApps().length);
831         }
832         if (!isSearching()) {
833             rebindAdapters();
834             if (mHasWorkApps) {
835                 mWorkManager.reset();
836             }
837         }
838 
839         mActivityContext.getStatsLogManager().logger()
840                 .withCardinality(mAllAppsStore.getApps().length)
841                 .log(LAUNCHER_ALLAPPS_COUNT);
842     }
843 
844     @Override
onInterceptTouchEvent(MotionEvent ev)845     public boolean onInterceptTouchEvent(MotionEvent ev) {
846         // The AllAppsContainerView houses the QSB and is hence visible from the Workspace
847         // Overview states. We shouldn't intercept for the scrubber in these cases.
848         if (!isInAllApps()) {
849             mTouchHandler = null;
850             return false;
851         }
852 
853         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
854             AllAppsRecyclerView rv = getActiveRecyclerView();
855             if (rv != null && rv.getScrollbar() != null
856                     && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) {
857                 mTouchHandler = rv.getScrollbar();
858             } else {
859                 mTouchHandler = null;
860             }
861         }
862         if (mTouchHandler != null) {
863             return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
864         }
865         return false;
866     }
867 
868     @Override
onTouchEvent(MotionEvent ev)869     public boolean onTouchEvent(MotionEvent ev) {
870         if (!isInAllApps()) {
871             return false;
872         }
873 
874         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
875             AllAppsRecyclerView rv = getActiveRecyclerView();
876             if (rv != null && rv.getScrollbar() != null
877                     && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) {
878                 mTouchHandler = rv.getScrollbar();
879             } else {
880                 mTouchHandler = null;
881 
882             }
883         }
884         if (mTouchHandler != null) {
885             mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
886             return true;
887         }
888         if (isSearching()
889                 && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) {
890             // if in search state, consume touch event.
891             return true;
892         }
893         return false;
894     }
895 
896     /** The current active recycler view (A-Z list from one of the profiles, or search results). */
getActiveRecyclerView()897     public AllAppsRecyclerView getActiveRecyclerView() {
898         if (isSearching()) {
899             return getSearchRecyclerView();
900         }
901         return getActiveAppsRecyclerView();
902     }
903 
904     /** The current apps recycler view in the container. */
getActiveAppsRecyclerView()905     private AllAppsRecyclerView getActiveAppsRecyclerView() {
906         if (!mUsingTabs || isPersonalTab()) {
907             return mAH.get(AdapterHolder.MAIN).mRecyclerView;
908         } else {
909             return mAH.get(AdapterHolder.WORK).mRecyclerView;
910         }
911     }
912 
913     /**
914      * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently
915      * hidden while searching.
916      **/
getAppsRecyclerViewContainer()917     protected View getAppsRecyclerViewContainer() {
918         return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
919     }
920 
921     /** The RV for search results, which is hidden while A-Z apps are visible. */
getSearchRecyclerView()922     public SearchRecyclerView getSearchRecyclerView() {
923         return mSearchRecyclerView;
924     }
925 
isPersonalTab()926     protected boolean isPersonalTab() {
927         return mViewPager == null || mViewPager.getNextPage() == 0;
928     }
929 
930     /**
931      * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does
932      * nothing.
933      */
switchToTab(int tab)934     public void switchToTab(int tab) {
935         if (mUsingTabs) {
936             mViewPager.setCurrentPage(tab);
937         }
938     }
939 
getLayoutInflater()940     public LayoutInflater getLayoutInflater() {
941         return LayoutInflater.from(getContext());
942     }
943 
944     @Override
onDropCompleted(View target, DragObject d, boolean success)945     public void onDropCompleted(View target, DragObject d, boolean success) {}
946 
947     @Override
setInsets(Rect insets)948     public void setInsets(Rect insets) {
949         mInsets.set(insets);
950         DeviceProfile grid = mActivityContext.getDeviceProfile();
951 
952         applyAdapterSideAndBottomPaddings(grid);
953 
954         MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
955         mlp.leftMargin = insets.left;
956         mlp.rightMargin = insets.right;
957         setLayoutParams(mlp);
958 
959         if (grid.isVerticalBarLayout()) {
960             setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
961         } else {
962             int topPadding = grid.allAppsTopPadding;
963             if (isSearchBarOnBottom() && !grid.isTablet) {
964                 topPadding += getResources().getDimensionPixelSize(
965                         R.dimen.all_apps_additional_top_padding_floating_search);
966             }
967             setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0);
968         }
969 
970         InsettableFrameLayout.dispatchInsets(this, insets);
971     }
972 
973     /**
974      * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed.
975      */
computeNavBarScrimHeight(WindowInsets insets)976     protected int computeNavBarScrimHeight(WindowInsets insets) {
977         return 0;
978     }
979 
980     /**
981      * Returns the current height of nav bar scrim
982      */
getNavBarScrimHeight()983     public int getNavBarScrimHeight() {
984         return mNavBarScrimHeight;
985     }
986 
987     @Override
dispatchApplyWindowInsets(WindowInsets insets)988     public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
989         mNavBarScrimHeight = computeNavBarScrimHeight(insets);
990         applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile());
991         return super.dispatchApplyWindowInsets(insets);
992     }
993 
994     @Override
dispatchDraw(Canvas canvas)995     protected void dispatchDraw(Canvas canvas) {
996         super.dispatchDraw(canvas);
997 
998         if (mNavBarScrimHeight > 0) {
999             canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
1000                     mNavBarScrimPaint);
1001         }
1002     }
1003 
updateSearchResultsVisibility()1004     protected void updateSearchResultsVisibility() {
1005         if (isSearching()) {
1006             getSearchRecyclerView().setVisibility(VISIBLE);
1007             getAppsRecyclerViewContainer().setVisibility(GONE);
1008             mHeader.setVisibility(GONE);
1009         } else {
1010             getSearchRecyclerView().setVisibility(GONE);
1011             getAppsRecyclerViewContainer().setVisibility(VISIBLE);
1012             mHeader.setVisibility(VISIBLE);
1013         }
1014         if (mHeader.isSetUp()) {
1015             mHeader.setActiveRV(getCurrentPage());
1016         }
1017     }
1018 
applyAdapterSideAndBottomPaddings(DeviceProfile grid)1019     private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) {
1020         int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight);
1021         mAH.forEach(adapterHolder -> {
1022             adapterHolder.mPadding.bottom = bottomPadding;
1023             adapterHolder.mPadding.left =
1024                     adapterHolder.mPadding.right = grid.allAppsLeftRightPadding;
1025             adapterHolder.applyPadding();
1026         });
1027     }
1028 
setDeviceManagementResources()1029     private void setDeviceManagementResources() {
1030         if (mActivityContext.getStringCache() != null) {
1031             Button personalTab = findViewById(R.id.tab_personal);
1032             personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab);
1033 
1034             Button workTab = findViewById(R.id.tab_work);
1035             workTab.setText(mActivityContext.getStringCache().allAppsWorkTab);
1036         }
1037     }
1038 
shouldShowTabs()1039     protected boolean shouldShowTabs() {
1040         return mHasWorkApps;
1041     }
1042 
1043     // Used by tests only
isDescendantViewVisible(int viewId)1044     private boolean isDescendantViewVisible(int viewId) {
1045         final View view = findViewById(viewId);
1046         if (view == null) return false;
1047 
1048         if (!view.isShown()) return false;
1049 
1050         return view.getGlobalVisibleRect(new Rect());
1051     }
1052 
1053     @VisibleForTesting
isPersonalTabVisible()1054     public boolean isPersonalTabVisible() {
1055         return isDescendantViewVisible(R.id.tab_personal);
1056     }
1057 
1058     @VisibleForTesting
isWorkTabVisible()1059     public boolean isWorkTabVisible() {
1060         return isDescendantViewVisible(R.id.tab_work);
1061     }
1062 
getSearchResultList()1063     public AlphabeticalAppsList<T> getSearchResultList() {
1064         return mAH.get(SEARCH).mAppsList;
1065     }
1066 
getFloatingHeaderView()1067     public FloatingHeaderView getFloatingHeaderView() {
1068         return mHeader;
1069     }
1070 
1071     @VisibleForTesting
getContentView()1072     public View getContentView() {
1073         return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer();
1074     }
1075 
1076     /** The current page visible in all apps. */
getCurrentPage()1077     public int getCurrentPage() {
1078         return isSearching()
1079                 ? SEARCH
1080                 : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage();
1081     }
1082 
1083     /**
1084      * Adds an update listener to animator that adds springs to the animation.
1085      */
addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity , float progress )1086     public void addSpringFromFlingUpdateListener(ValueAnimator animator,
1087             float velocity /* release velocity */,
1088             float progress /* portion of the distance to travel*/) {
1089         animator.addListener(new AnimatorListenerAdapter() {
1090             @Override
1091             public void onAnimationStart(Animator animator) {
1092                 float distance = (1 - progress) * getHeight(); // px
1093                 float settleVelocity = Math.min(0, distance
1094                         / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration())
1095                         + velocity);
1096                 absorbSwipeUpVelocity(Math.max(1000, Math.abs(
1097                         Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER))));
1098             }
1099         });
1100     }
1101 
1102     /** Invoked when the container is pulled. */
onPull(float deltaDistance, float displacement)1103     public void onPull(float deltaDistance, float displacement) {
1104         absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement);
1105         // Current motion spec is to actually push and not pull
1106         // on this surface. However, until EdgeEffect.onPush (b/190612804) is
1107         // implemented at view level, we will simply pull
1108     }
1109 
1110     @Override
getDrawingRect(Rect outRect)1111     public void getDrawingRect(Rect outRect) {
1112         super.getDrawingRect(outRect);
1113         outRect.offset(0, (int) getTranslationY());
1114     }
1115 
1116     @Override
setTranslationY(float translationY)1117     public void setTranslationY(float translationY) {
1118         super.setTranslationY(translationY);
1119         invalidateHeader();
1120     }
1121 
setScrimView(ScrimView scrimView)1122     public void setScrimView(ScrimView scrimView) {
1123         mScrimView = scrimView;
1124     }
1125 
1126     @Override
drawOnScrimWithScale(Canvas canvas, float scale)1127     public void drawOnScrimWithScale(Canvas canvas, float scale) {
1128         final boolean isTablet = mActivityContext.getDeviceProfile().isTablet;
1129         final View panel = mBottomSheetBackground;
1130         final float translationY = ((View) panel.getParent()).getTranslationY();
1131 
1132         final float horizontalScaleOffset = (1 - scale) * panel.getWidth() / 2;
1133         final float verticalScaleOffset = (1 - scale) * (panel.getHeight() - getHeight() / 2);
1134 
1135         final float topNoScale = panel.getTop() + translationY;
1136         final float topWithScale = topNoScale + verticalScaleOffset;
1137         final float leftWithScale = panel.getLeft() + horizontalScaleOffset;
1138         final float rightWithScale = panel.getRight() - horizontalScaleOffset;
1139         // Draw full background panel for tablets.
1140         if (isTablet) {
1141             mHeaderPaint.setColor(mBottomSheetBackgroundColor);
1142 
1143             mTmpRectF.set(
1144                     leftWithScale,
1145                     topWithScale,
1146                     rightWithScale,
1147                     panel.getBottom());
1148             mTmpPath.reset();
1149             mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW);
1150             canvas.drawPath(mTmpPath, mHeaderPaint);
1151         }
1152 
1153         if (DEBUG_HEADER_PROTECTION) {
1154             mHeaderPaint.setColor(Color.MAGENTA);
1155             mHeaderPaint.setAlpha(255);
1156         } else {
1157             mHeaderPaint.setColor(mHeaderColor);
1158             mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor)));
1159         }
1160         if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) {
1161             return;
1162         }
1163 
1164         // Draw header on background panel
1165         final float headerBottomNoScale =
1166                 getHeaderBottom() + getVisibleContainerView().getPaddingTop();
1167         final float headerHeightNoScale = headerBottomNoScale - topNoScale;
1168         final float headerBottomWithScaleOnTablet = topWithScale + headerHeightNoScale * scale;
1169         final float headerBottomOffset = (getVisibleContainerView().getHeight() * (1 - scale) / 2);
1170         final float headerBottomWithScaleOnPhone = headerBottomNoScale * scale + headerBottomOffset;
1171         final FloatingHeaderView headerView = getFloatingHeaderView();
1172         if (isTablet) {
1173             // Start adding header protection if search bar or tabs will attach to the top.
1174             if (!FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get() || mUsingTabs) {
1175                 mTmpRectF.set(
1176                         leftWithScale,
1177                         topWithScale,
1178                         rightWithScale,
1179                         headerBottomWithScaleOnTablet);
1180                 mTmpPath.reset();
1181                 mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW);
1182                 canvas.drawPath(mTmpPath, mHeaderPaint);
1183             }
1184         } else {
1185             canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint);
1186         }
1187 
1188         // If tab exist (such as work profile), extend header with tab height
1189         final int tabsHeight = headerView.getPeripheralProtectionHeight();
1190         if (mTabsProtectionAlpha > 0 && tabsHeight != 0) {
1191             if (DEBUG_HEADER_PROTECTION) {
1192                 mHeaderPaint.setColor(Color.BLUE);
1193                 mHeaderPaint.setAlpha(255);
1194             } else {
1195                 mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha));
1196             }
1197             float left = 0f;
1198             float right = canvas.getWidth();
1199             if (isTablet) {
1200                 left = mBottomSheetBackground.getLeft() + horizontalScaleOffset;
1201                 right = mBottomSheetBackground.getRight() - horizontalScaleOffset;
1202             }
1203 
1204             final float tabTopWithScale = isTablet
1205                     ? headerBottomWithScaleOnTablet
1206                     : headerBottomWithScaleOnPhone;
1207             final float tabBottomWithScale = tabTopWithScale + tabsHeight * scale;
1208 
1209             canvas.drawRect(
1210                     left,
1211                     tabTopWithScale,
1212                     right,
1213                     tabBottomWithScale,
1214                     mHeaderPaint);
1215         }
1216     }
1217 
1218     /**
1219      * redraws header protection
1220      */
invalidateHeader()1221     public void invalidateHeader() {
1222         if (mScrimView != null) {
1223             mScrimView.invalidate();
1224         }
1225     }
1226 
1227     /** Returns the position of the bottom edge of the header */
getHeaderBottom()1228     public int getHeaderBottom() {
1229         int bottom = (int) getTranslationY() + mHeader.getClipTop();
1230         if (isSearchBarOnBottom()) {
1231             if (mActivityContext.getDeviceProfile().isTablet) {
1232                 return bottom + mBottomSheetBackground.getTop();
1233             }
1234             return bottom;
1235         }
1236         return bottom + mHeader.getTop();
1237     }
1238 
1239     /**
1240      * Returns a view that denotes the visible part of all apps container view.
1241      */
getVisibleContainerView()1242     public View getVisibleContainerView() {
1243         return mActivityContext.getDeviceProfile().isTablet ? mBottomSheetBackground : this;
1244     }
1245 
onInitializeRecyclerView(RecyclerView rv)1246     protected void onInitializeRecyclerView(RecyclerView rv) {
1247         rv.addOnScrollListener(mScrollListener);
1248     }
1249 
1250     /** Holds a {@link BaseAllAppsAdapter} and related fields. */
1251     public class AdapterHolder {
1252         public static final int MAIN = 0;
1253         public static final int WORK = 1;
1254         public static final int SEARCH = 2;
1255 
1256         private final int mType;
1257         public final BaseAllAppsAdapter<T> mAdapter;
1258         final RecyclerView.LayoutManager mLayoutManager;
1259         final AlphabeticalAppsList<T> mAppsList;
1260         final Rect mPadding = new Rect();
1261         AllAppsRecyclerView mRecyclerView;
1262 
AdapterHolder(int type, AlphabeticalAppsList<T> appsList)1263         AdapterHolder(int type, AlphabeticalAppsList<T> appsList) {
1264             mType = type;
1265             mAppsList = appsList;
1266             mAdapter = createAdapter(mAppsList);
1267             mAppsList.setAdapter(mAdapter);
1268             mLayoutManager = mAdapter.getLayoutManager();
1269         }
1270 
setup(@onNull View rv, @Nullable Predicate<ItemInfo> matcher)1271         void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) {
1272             mAppsList.updateItemFilter(matcher);
1273             mRecyclerView = (AllAppsRecyclerView) rv;
1274             mRecyclerView.bindFastScrollbar(mFastScroller);
1275             mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory());
1276             mRecyclerView.setApps(mAppsList);
1277             mRecyclerView.setLayoutManager(mLayoutManager);
1278             mRecyclerView.setAdapter(mAdapter);
1279             mRecyclerView.setHasFixedSize(true);
1280             // No animations will occur when changes occur to the items in this RecyclerView.
1281             mRecyclerView.setItemAnimator(null);
1282             onInitializeRecyclerView(mRecyclerView);
1283             FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView);
1284             mRecyclerView.addItemDecoration(focusedItemDecorator);
1285             mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
1286             applyPadding();
1287         }
1288 
applyPadding()1289         void applyPadding() {
1290             if (mRecyclerView != null) {
1291                 int bottomOffset = 0;
1292                 if (isWork() && mWorkManager.getWorkModeSwitch() != null) {
1293                     bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
1294                 }
1295                 mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right,
1296                         mPadding.bottom + bottomOffset);
1297             }
1298         }
1299 
isWork()1300         private boolean isWork() {
1301             return mType == WORK;
1302         }
1303 
isSearch()1304         private boolean isSearch() {
1305             return mType == SEARCH;
1306         }
1307     }
1308 }
1309