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.Flags.enableExpandingPauseWorkButton;
19 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
20 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
21 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
22 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
23 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
24 import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
25 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
29 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
30 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
31 
32 import android.animation.Animator;
33 import android.animation.AnimatorListenerAdapter;
34 import android.animation.ValueAnimator;
35 import android.content.Context;
36 import android.graphics.Canvas;
37 import android.graphics.Color;
38 import android.graphics.Outline;
39 import android.graphics.Paint;
40 import android.graphics.Path;
41 import android.graphics.Path.Direction;
42 import android.graphics.Point;
43 import android.graphics.Rect;
44 import android.graphics.RectF;
45 import android.os.Bundle;
46 import android.os.Parcelable;
47 import android.os.Process;
48 import android.os.UserManager;
49 import android.util.AttributeSet;
50 import android.util.FloatProperty;
51 import android.util.Log;
52 import android.util.SparseArray;
53 import android.view.KeyEvent;
54 import android.view.LayoutInflater;
55 import android.view.MotionEvent;
56 import android.view.View;
57 import android.view.ViewGroup;
58 import android.view.ViewOutlineProvider;
59 import android.view.WindowInsets;
60 import android.widget.Button;
61 import android.widget.RelativeLayout;
62 
63 import androidx.annotation.NonNull;
64 import androidx.annotation.Nullable;
65 import androidx.annotation.Px;
66 import androidx.annotation.VisibleForTesting;
67 import androidx.core.graphics.ColorUtils;
68 import androidx.recyclerview.widget.RecyclerView;
69 
70 import com.android.launcher3.DeviceProfile;
71 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
72 import com.android.launcher3.DragSource;
73 import com.android.launcher3.DropTarget.DragObject;
74 import com.android.launcher3.Insettable;
75 import com.android.launcher3.InsettableFrameLayout;
76 import com.android.launcher3.R;
77 import com.android.launcher3.Utilities;
78 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
79 import com.android.launcher3.allapps.search.AllAppsSearchUiDelegate;
80 import com.android.launcher3.allapps.search.SearchAdapterProvider;
81 import com.android.launcher3.config.FeatureFlags;
82 import com.android.launcher3.keyboard.FocusedItemDecorator;
83 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
84 import com.android.launcher3.model.StringCache;
85 import com.android.launcher3.model.data.ItemInfo;
86 import com.android.launcher3.pm.UserCache;
87 import com.android.launcher3.recyclerview.AllAppsRecyclerViewPool;
88 import com.android.launcher3.util.ItemInfoMatcher;
89 import com.android.launcher3.util.Preconditions;
90 import com.android.launcher3.util.Themes;
91 import com.android.launcher3.views.ActivityContext;
92 import com.android.launcher3.views.BaseDragLayer;
93 import com.android.launcher3.views.RecyclerViewFastScroller;
94 import com.android.launcher3.views.ScrimView;
95 import com.android.launcher3.views.SpringRelativeLayout;
96 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip;
97 
98 import java.util.ArrayList;
99 import java.util.Arrays;
100 import java.util.List;
101 import java.util.Optional;
102 import java.util.function.Predicate;
103 import java.util.stream.Stream;
104 
105 /**
106  * All apps container view with search support for use in a dragging activity.
107  *
108  * @param <T> Type of context inflating all apps.
109  */
110 public class ActivityAllAppsContainerView<T extends Context & ActivityContext>
111         extends SpringRelativeLayout implements DragSource, Insettable,
112         OnDeviceProfileChangeListener, PersonalWorkSlidingTabStrip.OnActivePageChangedListener,
113         ScrimView.ScrimDrawingController {
114 
115 
116     public static final FloatProperty<ActivityAllAppsContainerView<?>> BOTTOM_SHEET_ALPHA =
117             new FloatProperty<>("bottomSheetAlpha") {
118                 @Override
119                 public Float get(ActivityAllAppsContainerView<?> containerView) {
120                     return containerView.mBottomSheetAlpha;
121                 }
122 
123                 @Override
124                 public void setValue(ActivityAllAppsContainerView<?> containerView, float v) {
125                     containerView.setBottomSheetAlpha(v);
126                 }
127             };
128 
129     public static final float PULL_MULTIPLIER = .02f;
130     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
131     protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
132     private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300;
133     // Render the header protection at all times to debug clipping issues.
134     private static final boolean DEBUG_HEADER_PROTECTION = false;
135     /** Context of an activity or window that is inflating this container. */
136 
137     protected final T mActivityContext;
138     protected final List<AdapterHolder> mAH;
139     protected final Predicate<ItemInfo> mPersonalMatcher = ItemInfoMatcher.ofUser(
140             Process.myUserHandle());
141     protected WorkProfileManager mWorkManager;
142     protected final PrivateProfileManager mPrivateProfileManager;
143     protected final Point mFastScrollerOffset = new Point();
144     protected final int mScrimColor;
145     protected final float mHeaderThreshold;
146     protected final AllAppsSearchUiDelegate mSearchUiDelegate;
147 
148     // Used to animate Search results out and A-Z apps in, or vice-versa.
149     private final SearchTransitionController mSearchTransitionController;
150     private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
151     private final Rect mInsets = new Rect();
152     private final AllAppsStore<T> mAllAppsStore;
153     private final RecyclerView.OnScrollListener mScrollListener =
154             new RecyclerView.OnScrollListener() {
155                 @Override
156                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
157                     updateHeaderScroll(recyclerView.computeVerticalScrollOffset());
158                 }
159             };
160     private final Paint mNavBarScrimPaint;
161     private final int mHeaderProtectionColor;
162     private final int mPrivateSpaceBottomExtraSpace;
163     private final Path mTmpPath = new Path();
164     private final RectF mTmpRectF = new RectF();
165     protected AllAppsPagedView mViewPager;
166     protected FloatingHeaderView mHeader;
167     protected View mBottomSheetBackground;
168     protected RecyclerViewFastScroller mFastScroller;
169 
170     /**
171      * View that defines the search box. Result is rendered inside {@link #mSearchRecyclerView}.
172      */
173     protected View mSearchContainer;
174     protected SearchUiManager mSearchUiManager;
175     protected boolean mUsingTabs;
176     protected RecyclerViewFastScroller mTouchHandler;
177 
178     /** {@code true} when rendered view is in search state instead of the scroll state. */
179     private boolean mIsSearching;
180     private boolean mRebindAdaptersAfterSearchAnimation;
181     private int mNavBarScrimHeight = 0;
182     private SearchRecyclerView mSearchRecyclerView;
183     protected SearchAdapterProvider<?> mMainAdapterProvider;
184     private View mBottomSheetHandleArea;
185     private boolean mHasWorkApps;
186     private boolean mHasPrivateApps;
187     private float[] mBottomSheetCornerRadii;
188     private ScrimView mScrimView;
189     private int mHeaderColor;
190     private int mBottomSheetBackgroundColor;
191     private float mBottomSheetAlpha = 1f;
192     private boolean mForceBottomSheetVisible;
193     private int mTabsProtectionAlpha;
194     @Nullable private AllAppsTransitionController mAllAppsTransitionController;
195 
ActivityAllAppsContainerView(Context context)196     public ActivityAllAppsContainerView(Context context) {
197         this(context, null);
198     }
199 
ActivityAllAppsContainerView(Context context, AttributeSet attrs)200     public ActivityAllAppsContainerView(Context context, AttributeSet attrs) {
201         this(context, attrs, 0);
202     }
203 
ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr)204     public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
205         super(context, attrs, defStyleAttr);
206         mActivityContext = ActivityContext.lookupContext(context);
207         mAllAppsStore = new AllAppsStore<>(mActivityContext);
208 
209         mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
210         mHeaderThreshold = getResources().getDimensionPixelSize(
211                 R.dimen.dynamic_grid_cell_border_spacing);
212         mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor);
213 
214         mWorkManager = new WorkProfileManager(
215                 mActivityContext.getSystemService(UserManager.class),
216                 this,
217                 mActivityContext.getStatsLogManager(),
218                 UserCache.INSTANCE.get(mActivityContext));
219         mPrivateProfileManager = new PrivateProfileManager(
220                 mActivityContext.getSystemService(UserManager.class),
221                 this,
222                 mActivityContext.getStatsLogManager(),
223                 UserCache.INSTANCE.get(mActivityContext));
224         mPrivateSpaceBottomExtraSpace = context.getResources().getDimensionPixelSize(
225                 R.dimen.ps_extra_bottom_padding);
226         mAH = Arrays.asList(null, null, null);
227         mNavBarScrimPaint = new Paint();
228         mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext));
229 
230         AllAppsStore.OnUpdateListener onAppsUpdated = this::onAppsUpdated;
231         mAllAppsStore.addUpdateListener(onAppsUpdated);
232 
233         // This is a focus listener that proxies focus from a view into the list view.  This is to
234         // work around the search box from getting first focus and showing the cursor.
235         setOnFocusChangeListener((v, hasFocus) -> {
236             if (hasFocus && getActiveRecyclerView() != null) {
237                 getActiveRecyclerView().requestFocus();
238             }
239         });
240         mSearchUiDelegate = createSearchUiDelegate();
241         initContent();
242 
243         mSearchTransitionController = new SearchTransitionController(this);
244     }
245 
246     /** Creates the delegate for initializing search. */
createSearchUiDelegate()247     protected AllAppsSearchUiDelegate createSearchUiDelegate() {
248         return new AllAppsSearchUiDelegate(this);
249     }
250 
getSearchUiDelegate()251     public AllAppsSearchUiDelegate getSearchUiDelegate() {
252         return mSearchUiDelegate;
253     }
254 
255     /**
256      * Initializes the view hierarchy and internal variables. Any initialization which actually uses
257      * these members should be done in {@link #onFinishInflate()}.
258      * In terms of subclass initialization, the following would be parallel order for activity:
259      *   initContent -> onPreCreate
260      *   constructor/init -> onCreate
261      *   onFinishInflate -> onPostCreate
262      */
initContent()263     protected void initContent() {
264         mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider();
265 
266         mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN,
267                 new AlphabeticalAppsList<>(mActivityContext,
268                         mAllAppsStore,
269                         null,
270                         mPrivateProfileManager)));
271         mAH.set(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK,
272                 new AlphabeticalAppsList<>(mActivityContext, mAllAppsStore, mWorkManager, null)));
273         mAH.set(SEARCH, new AdapterHolder(SEARCH,
274                 new AlphabeticalAppsList<>(mActivityContext, null, null, null)));
275 
276         getLayoutInflater().inflate(R.layout.all_apps_content, this);
277         mHeader = findViewById(R.id.all_apps_header);
278         mBottomSheetBackground = findViewById(R.id.bottom_sheet_background);
279         mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area);
280         mSearchRecyclerView = findViewById(R.id.search_results_list_view);
281         mFastScroller = findViewById(R.id.fast_scroller);
282         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
283 
284         mSearchContainer = inflateSearchBar();
285         if (!isSearchBarFloating()) {
286             // Add the search box above everything else in this container (if the flag is enabled,
287             // it's added to drag layer in onAttach instead).
288             addView(mSearchContainer);
289         }
290         mSearchUiManager = (SearchUiManager) mSearchContainer;
291     }
292 
293     @Override
onFinishInflate()294     protected void onFinishInflate() {
295         super.onFinishInflate();
296 
297         mAH.get(SEARCH).setup(mSearchRecyclerView,
298                 /* Filter out A-Z apps */ itemInfo -> false);
299         rebindAdapters(true /* force */);
300         float cornerRadius = Themes.getDialogCornerRadius(getContext());
301         mBottomSheetCornerRadii = new float[]{
302                 cornerRadius,
303                 cornerRadius, // Top left radius in px
304                 cornerRadius,
305                 cornerRadius, // Top right radius in px
306                 0,
307                 0, // Bottom right
308                 0,
309                 0 // Bottom left
310         };
311         mBottomSheetBackgroundColor =
312                 Themes.getAttrColor(getContext(), R.attr.materialColorSurfaceDim);
313         updateBackgroundVisibility(mActivityContext.getDeviceProfile());
314         mSearchUiManager.initializeSearch(this);
315     }
316 
317     @Override
onAttachedToWindow()318     protected void onAttachedToWindow() {
319         super.onAttachedToWindow();
320         if (isSearchBarFloating()) {
321             // Note: for Taskbar this is removed in TaskbarAllAppsController#cleanUpOverlay when the
322             // panel is closed. Can't do so in onDetach because we are also a child of drag layer
323             // so can't remove its views during that dispatch.
324             mActivityContext.getDragLayer().addView(mSearchContainer);
325             mSearchUiDelegate.onInitializeSearchBar();
326         }
327         mActivityContext.addOnDeviceProfileChangeListener(this);
328     }
329 
330     @Override
onDetachedFromWindow()331     protected void onDetachedFromWindow() {
332         super.onDetachedFromWindow();
333         mActivityContext.removeOnDeviceProfileChangeListener(this);
334     }
335 
getSearchUiManager()336     public SearchUiManager getSearchUiManager() {
337         return mSearchUiManager;
338     }
339 
getBottomSheetBackground()340     public View getBottomSheetBackground() {
341         return mBottomSheetBackground;
342     }
343 
344     /**
345      * Temporarily force the bottom sheet to be visible on non-tablets.
346      *
347      * @param force {@code true} means bottom sheet will be visible on phones until {@code reset()}.
348      */
forceBottomSheetVisible(boolean force)349     public void forceBottomSheetVisible(boolean force) {
350         mForceBottomSheetVisible = force;
351         updateBackgroundVisibility(mActivityContext.getDeviceProfile());
352     }
353 
getSearchView()354     public View getSearchView() {
355         return mSearchContainer;
356     }
357 
358     /** Invoke when the current search session is finished. */
onClearSearchResult()359     public void onClearSearchResult() {
360         getMainAdapterProvider().clearHighlightedItem();
361         animateToSearchState(false);
362         rebindAdapters();
363     }
364 
365     /**
366      * Sets results list for search
367      */
setSearchResults(ArrayList<AdapterItem> results)368     public void setSearchResults(ArrayList<AdapterItem> results) {
369         getMainAdapterProvider().clearHighlightedItem();
370         if (getSearchResultList().setSearchResults(results)) {
371             getSearchRecyclerView().onSearchResultsChanged();
372         }
373         if (results != null) {
374             animateToSearchState(true);
375         }
376     }
377 
378     /**
379      * Sets results list for search.
380      *
381      * @param searchResultCode indicates if the result is final or intermediate for a given query
382      *                         since we can get search results from multiple sources.
383      */
setSearchResults(ArrayList<AdapterItem> results, int searchResultCode)384     public void setSearchResults(ArrayList<AdapterItem> results, int searchResultCode) {
385         setSearchResults(results);
386         mSearchUiDelegate.onSearchResultsChanged(results, searchResultCode);
387     }
388 
animateToSearchState(boolean goingToSearch)389     private void animateToSearchState(boolean goingToSearch) {
390         animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS);
391     }
392 
setAllAppsTransitionController( AllAppsTransitionController allAppsTransitionController)393     public void setAllAppsTransitionController(
394             AllAppsTransitionController allAppsTransitionController) {
395         mAllAppsTransitionController = allAppsTransitionController;
396     }
397 
animateToSearchState(boolean goingToSearch, long durationMs)398     void animateToSearchState(boolean goingToSearch, long durationMs) {
399         if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) {
400             return;
401         }
402         mFastScroller.setVisibility(goingToSearch ? INVISIBLE : VISIBLE);
403         if (goingToSearch) {
404             // Fade out the button to pause work apps.
405             mWorkManager.onActivePageChanged(SEARCH);
406         } else if (mAllAppsTransitionController != null) {
407             // If exiting search, revert predictive back scale on all apps
408             mAllAppsTransitionController.animateAllAppsToNoScale();
409         }
410         mSearchTransitionController.animateToState(goingToSearch, durationMs,
411                 /* onEndRunnable = */ () -> {
412                     mIsSearching = goingToSearch;
413                     updateSearchResultsVisibility();
414                     int previousPage = getCurrentPage();
415                     if (mRebindAdaptersAfterSearchAnimation) {
416                         rebindAdapters(false);
417                         mRebindAdaptersAfterSearchAnimation = false;
418                     }
419 
420                     if (goingToSearch) {
421                         mSearchUiDelegate.onAnimateToSearchStateCompleted();
422                     } else {
423                         setSearchResults(null);
424                         if (mViewPager != null) {
425                             mViewPager.setCurrentPage(previousPage);
426                         }
427                         onActivePageChanged(previousPage);
428                     }
429                 });
430     }
431 
shouldContainerScroll(MotionEvent ev)432     public boolean shouldContainerScroll(MotionEvent ev) {
433         BaseDragLayer dragLayer = mActivityContext.getDragLayer();
434         // IF the MotionEvent is inside the search box or handle area, and the container keeps on
435         // receiving touch input, container should move down.
436         if (dragLayer.isEventOverView(mSearchContainer, ev)
437                 || dragLayer.isEventOverView(mBottomSheetHandleArea, ev)) {
438             return true;
439         }
440         AllAppsRecyclerView rv = getActiveRecyclerView();
441         if (rv == null) {
442             return true;
443         }
444         if (rv.getScrollbar() != null
445                 && rv.getScrollbar().getThumbOffsetY() >= 0
446                 && dragLayer.isEventOverView(rv.getScrollbar(), ev)) {
447             return false;
448         }
449         // Scroll if not within the container view (e.g. over large-screen scrim).
450         if (!dragLayer.isEventOverView(getVisibleContainerView(), ev)) {
451             return true;
452         }
453         return rv.shouldContainerScroll(ev, dragLayer);
454     }
455 
456     /**
457      * Resets the UI to be ready for fresh interactions in the future. Exits search and returns to
458      * A-Z apps list.
459      *
460      * @param animate Whether to animate the header during the reset (e.g. switching profile tabs).
461      */
reset(boolean animate)462     public void reset(boolean animate) {
463         reset(animate, true);
464     }
465 
466     /**
467      * Resets the UI to be ready for fresh interactions in the future.
468      *
469      * @param animate Whether to animate the header during the reset (e.g. switching profile tabs).
470      * @param exitSearch Whether to force exit the search state and return to A-Z apps list.
471      */
reset(boolean animate, boolean exitSearch)472     public void reset(boolean animate, boolean exitSearch) {
473         // Scroll Main and Work RV to top. Search RV is done in `resetSearch`.
474         for (int i = 0; i < mAH.size(); i++) {
475             if (i != SEARCH && mAH.get(i).mRecyclerView != null) {
476                 mAH.get(i).mRecyclerView.scrollToTop();
477             }
478         }
479         if (mTouchHandler != null) {
480             mTouchHandler.endFastScrolling();
481         }
482         if (mHeader != null && mHeader.getVisibility() == VISIBLE) {
483             mHeader.reset(animate);
484         }
485         forceBottomSheetVisible(false);
486         // Reset the base recycler view after transitioning home.
487         updateHeaderScroll(0);
488         if (exitSearch) {
489             // Reset the search bar and search RV after transitioning home.
490             MAIN_EXECUTOR.getHandler().post(mSearchUiManager::resetSearch);
491         }
492         if (isSearching()) {
493             mWorkManager.reset();
494         }
495     }
496 
497     /**
498      * Exits search and returns to A-Z apps list. Scroll to the private space header.
499      */
resetAndScrollToPrivateSpaceHeader()500     public void resetAndScrollToPrivateSpaceHeader() {
501         // Animate to A-Z with 0 time to reset the animation with proper state management.
502         // We can't rely on `animateToSearchState` with delay inside `resetSearch` because that will
503         // conflict with following scrolling to bottom, so we need it with 0 time here.
504         animateToSearchState(false, 0);
505 
506         MAIN_EXECUTOR.getHandler().post(() -> {
507             // Reset the search bar after transitioning home.
508             // When `resetSearch` is called after `animateToSearchState` is finished, the inside
509             // `animateToSearchState` with delay is a just no-op and return early.
510             mSearchUiManager.resetSearch();
511             // Switch to the main tab
512             switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN);
513             // Scroll to bottom
514             if (mPrivateProfileManager != null) {
515                 mPrivateProfileManager.scrollForHeaderToBeVisibleInContainer(
516                         getActiveAppsRecyclerView(),
517                         getPersonalAppList().getAdapterItems(),
518                         mPrivateProfileManager.getPsHeaderHeight(),
519                         mActivityContext.getDeviceProfile().allAppsCellHeightPx);
520             }
521         });
522     }
523 
524     @Override
dispatchKeyEvent(KeyEvent event)525     public boolean dispatchKeyEvent(KeyEvent event) {
526         mSearchUiManager.preDispatchKeyEvent(event);
527         return super.dispatchKeyEvent(event);
528     }
529 
getDescription()530     public String getDescription() {
531         if (!mUsingTabs && isSearching()) {
532             return getContext().getString(R.string.all_apps_search_results);
533         } else {
534             StringCache cache = mActivityContext.getStringCache();
535             if (mUsingTabs) {
536                 if (cache != null) {
537                     return isPersonalTab()
538                             ? cache.allAppsPersonalTabAccessibility
539                             : cache.allAppsWorkTabAccessibility;
540                 } else {
541                     return isPersonalTab()
542                             ? getContext().getString(R.string.all_apps_button_personal_label)
543                             : getContext().getString(R.string.all_apps_button_work_label);
544                 }
545             }
546             return getContext().getString(R.string.all_apps_button_label);
547         }
548     }
549 
isSearching()550     public boolean isSearching() {
551         return mIsSearching;
552     }
553 
554     @Override
onActivePageChanged(int currentActivePage)555     public void onActivePageChanged(int currentActivePage) {
556         if (mSearchTransitionController.isRunning()) {
557             // Will be called at the end of the animation.
558             return;
559         }
560         if (currentActivePage != SEARCH) {
561             mActivityContext.hideKeyboard();
562         }
563         if (mAH.get(currentActivePage).mRecyclerView != null) {
564             mAH.get(currentActivePage).mRecyclerView.bindFastScrollbar(mFastScroller);
565         }
566         // Header keeps track of active recycler view to properly render header protection.
567         mHeader.setActiveRV(currentActivePage);
568         reset(true /* animate */, !isSearching() /* exitSearch */);
569 
570         mWorkManager.onActivePageChanged(currentActivePage);
571     }
572 
rebindAdapters()573     protected void rebindAdapters() {
574         rebindAdapters(false /* force */);
575     }
576 
rebindAdapters(boolean force)577     protected void rebindAdapters(boolean force) {
578         if (mSearchTransitionController.isRunning()) {
579             mRebindAdaptersAfterSearchAnimation = true;
580             return;
581         }
582         updateSearchResultsVisibility();
583 
584         boolean showTabs = shouldShowTabs();
585         if (showTabs == mUsingTabs && !force) {
586             return;
587         }
588 
589         // replaceAppsRVcontainer() needs to use both mUsingTabs value to remove the old view AND
590         // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen
591         // after this call.
592         replaceAppsRVContainer(showTabs);
593         mUsingTabs = showTabs;
594 
595         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView);
596         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
597         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
598 
599         final AllAppsRecyclerView mainRecyclerView;
600         final AllAppsRecyclerView workRecyclerView;
601         if (mUsingTabs) {
602             mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0);
603             workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1);
604             mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
605             mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getItemInfoMatcher());
606             workRecyclerView.setId(R.id.apps_list_view_work);
607             if (enableExpandingPauseWorkButton()
608                     || FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
609                 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
610                         mWorkManager.newScrollListener());
611             }
612             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
613             findViewById(R.id.tab_personal)
614                     .setOnClickListener((View view) -> {
615                         if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
616                             mActivityContext.getStatsLogManager().logger()
617                                     .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
618                         }
619                     });
620             findViewById(R.id.tab_work)
621                     .setOnClickListener((View view) -> {
622                         if (mViewPager.snapToPage(AdapterHolder.WORK)) {
623                             mActivityContext.getStatsLogManager().logger()
624                                     .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
625                         }
626                     });
627             setDeviceManagementResources();
628             if (mHeader.isSetUp()) {
629                 onActivePageChanged(mViewPager.getNextPage());
630             }
631         } else {
632             mainRecyclerView = findViewById(R.id.apps_list_view);
633             workRecyclerView = null;
634             mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
635             mAH.get(AdapterHolder.WORK).mRecyclerView = null;
636         }
637         setUpCustomRecyclerViewPool(
638                 mainRecyclerView,
639                 workRecyclerView,
640                 mAllAppsStore.getRecyclerViewPool());
641         setupHeader();
642 
643         if (isSearchBarFloating()) {
644             // Keep the scroller above the search bar.
645             RelativeLayout.LayoutParams scrollerLayoutParams =
646                     (LayoutParams) mFastScroller.getLayoutParams();
647             scrollerLayoutParams.bottomMargin = mSearchContainer.getHeight()
648                     + getResources().getDimensionPixelSize(
649                             R.dimen.fastscroll_bottom_margin_floating_search);
650         }
651 
652         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView);
653         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
654         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
655     }
656 
657     /**
658      * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom
659      * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}.
660      *
661      * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because
662      * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE),
663      * thus we cannot rely on layout pass to update pool size.
664      */
setUpCustomRecyclerViewPool( @onNull AllAppsRecyclerView mainRecyclerView, @Nullable AllAppsRecyclerView workRecyclerView, @NonNull AllAppsRecyclerViewPool recycledViewPool)665     private static void setUpCustomRecyclerViewPool(
666             @NonNull AllAppsRecyclerView mainRecyclerView,
667             @Nullable AllAppsRecyclerView workRecyclerView,
668             @NonNull AllAppsRecyclerViewPool recycledViewPool) {
669         if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
670             return;
671         }
672         final boolean hasWorkProfile = workRecyclerView != null;
673         recycledViewPool.setHasWorkProfile(hasWorkProfile);
674         mainRecyclerView.setRecycledViewPool(recycledViewPool);
675         if (workRecyclerView != null) {
676             workRecyclerView.setRecycledViewPool(recycledViewPool);
677         }
678         if (ALL_APPS_GONE_VISIBILITY.get()) {
679             mainRecyclerView.updatePoolSize(hasWorkProfile);
680         }
681     }
682 
replaceAppsRVContainer(boolean showTabs)683     private void replaceAppsRVContainer(boolean showTabs) {
684         for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) {
685             AdapterHolder adapterHolder = mAH.get(i);
686             if (adapterHolder.mRecyclerView != null) {
687                 adapterHolder.mRecyclerView.setLayoutManager(null);
688                 adapterHolder.mRecyclerView.setAdapter(null);
689             }
690         }
691         View oldView = getAppsRecyclerViewContainer();
692         int index = indexOfChild(oldView);
693         removeView(oldView);
694         int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
695         final View rvContainer = getLayoutInflater().inflate(layout, this, false);
696         addView(rvContainer, index);
697         if (showTabs) {
698             mViewPager = (AllAppsPagedView) rvContainer;
699             mViewPager.initParentViews(this);
700             mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
701             mViewPager.setOutlineProvider(new ViewOutlineProvider() {
702                 @Override
703                 public void getOutline(View view, Outline outline) {
704                     @Px final int bottomOffsetPx =
705                             (int) (ActivityAllAppsContainerView.this.getMeasuredHeight()
706                                     * PREDICTIVE_BACK_MIN_SCALE);
707                     outline.setRect(
708                             0,
709                             0,
710                             view.getMeasuredWidth(),
711                             view.getMeasuredHeight() + bottomOffsetPx);
712                 }
713             });
714 
715             mWorkManager.reset();
716             post(() -> mAH.get(AdapterHolder.WORK).applyPadding());
717 
718         } else {
719             mWorkManager.detachWorkModeSwitch();
720             mViewPager = null;
721         }
722 
723         removeCustomRules(rvContainer);
724         removeCustomRules(getSearchRecyclerView());
725         if (!isSearchSupported()) {
726             layoutWithoutSearchContainer(rvContainer, showTabs);
727         } else if (isSearchBarFloating()) {
728             alignParentTop(rvContainer, showTabs);
729             alignParentTop(getSearchRecyclerView(), /* tabs= */ false);
730         } else {
731             layoutBelowSearchContainer(rvContainer, showTabs);
732             layoutBelowSearchContainer(getSearchRecyclerView(), /* tabs= */ false);
733         }
734 
735         updateSearchResultsVisibility();
736     }
737 
setupHeader()738     void setupHeader() {
739         mHeader.setVisibility(View.VISIBLE);
740         boolean tabsHidden = !mUsingTabs;
741         mHeader.setup(
742                 mAH.get(AdapterHolder.MAIN).mRecyclerView,
743                 mAH.get(AdapterHolder.WORK).mRecyclerView,
744                 (SearchRecyclerView) mAH.get(SEARCH).mRecyclerView,
745                 getCurrentPage(),
746                 tabsHidden);
747 
748         int padding = mHeader.getMaxTranslation();
749         mAH.forEach(adapterHolder -> {
750             adapterHolder.mPadding.top = padding;
751             adapterHolder.applyPadding();
752             if (adapterHolder.mRecyclerView != null) {
753                 adapterHolder.mRecyclerView.scrollToTop();
754             }
755         });
756 
757         removeCustomRules(mHeader);
758         if (!isSearchSupported()) {
759             layoutWithoutSearchContainer(mHeader, false /* includeTabsMargin */);
760         } else if (isSearchBarFloating()) {
761             alignParentTop(mHeader, false /* includeTabsMargin */);
762         } else {
763             layoutBelowSearchContainer(mHeader, false /* includeTabsMargin */);
764         }
765     }
766 
updateHeaderScroll(int scrolledOffset)767     protected void updateHeaderScroll(int scrolledOffset) {
768         float prog1 = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
769         int headerColor = getHeaderColor(prog1);
770         int tabsAlpha = mHeader.getPeripheralProtectionHeight(/* expectedHeight */ false) == 0 ? 0
771                 : (int) (Utilities.boundToRange(
772                         (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f)
773                         * 255);
774         if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) {
775             mHeaderColor = headerColor;
776             mTabsProtectionAlpha = tabsAlpha;
777             invalidateHeader();
778         }
779         if (mSearchUiManager.getEditText() == null) {
780             return;
781         }
782 
783         float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
784         boolean bgVisible = mSearchUiManager.getBackgroundVisibility();
785         if (scrolledOffset == 0 && !isSearching()) {
786             bgVisible = true;
787         } else if (scrolledOffset > mHeaderThreshold) {
788             bgVisible = false;
789         }
790         mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog);
791     }
792 
getHeaderColor(float blendRatio)793     protected int getHeaderColor(float blendRatio) {
794         return ColorUtils.setAlphaComponent(
795                 ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, blendRatio),
796                 (int) (mSearchContainer.getAlpha() * 255));
797     }
798 
799     /**
800      * @return true if the search bar is floating above this container (at the bottom of the screen)
801      */
isSearchBarFloating()802     protected boolean isSearchBarFloating() {
803         return mSearchUiDelegate.isSearchBarFloating();
804     }
805 
806     /**
807      * Whether the <em>floating</em> search bar should appear as a small pill when not focused.
808      * <p>
809      * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
810      * makes sense to use that method to derive an appropriate value for the current/target state.
811      */
shouldFloatingSearchBarBePillWhenUnfocused()812     public boolean shouldFloatingSearchBarBePillWhenUnfocused() {
813         return false;
814     }
815 
816     /**
817      * How far from the bottom of the screen the <em>floating</em> search bar should rest when the
818      * IME is not present.
819      * <p>
820      * To hide offscreen, use a negative value.
821      * <p>
822      * Note: if the provided value is non-negative but less than the current bottom insets, the
823      * insets will be applied. As such, you can use 0 to default to this.
824      * <p>
825      * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
826      * makes sense to use that method to derive an appropriate value for the current/target state.
827      */
getFloatingSearchBarRestingMarginBottom()828     public int getFloatingSearchBarRestingMarginBottom() {
829         return 0;
830     }
831 
832     /**
833      * How far from the start of the screen the <em>floating</em> search bar should rest.
834      * <p>
835      * To use original margin, return a negative value.
836      * <p>
837      * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
838      * makes sense to use that method to derive an appropriate value for the current/target state.
839      */
getFloatingSearchBarRestingMarginStart()840     public int getFloatingSearchBarRestingMarginStart() {
841         DeviceProfile dp = mActivityContext.getDeviceProfile();
842         return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin(mActivityContext);
843     }
844 
845     /**
846      * How far from the end of the screen the <em>floating</em> search bar should rest.
847      * <p>
848      * To use original margin, return a negative value.
849      * <p>
850      * Note: This method mirrors one in LauncherState. For subclasses that use Launcher, it likely
851      * makes sense to use that method to derive an appropriate value for the current/target state.
852      */
getFloatingSearchBarRestingMarginEnd()853     public int getFloatingSearchBarRestingMarginEnd() {
854         DeviceProfile dp = mActivityContext.getDeviceProfile();
855         return dp.allAppsLeftRightMargin + dp.getAllAppsIconStartMargin(mActivityContext);
856     }
857 
layoutBelowSearchContainer(View v, boolean includeTabsMargin)858     private void layoutBelowSearchContainer(View v, boolean includeTabsMargin) {
859         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
860             return;
861         }
862 
863         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
864         layoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.search_container_all_apps);
865 
866         int topMargin = getContext().getResources().getDimensionPixelSize(
867                 R.dimen.all_apps_header_top_margin);
868         if (includeTabsMargin) {
869             topMargin += getContext().getResources().getDimensionPixelSize(
870                     R.dimen.all_apps_header_pill_height);
871         }
872         layoutParams.topMargin = topMargin;
873     }
874 
alignParentTop(View v, boolean includeTabsMargin)875     private void alignParentTop(View v, boolean includeTabsMargin) {
876         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
877             return;
878         }
879 
880         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
881         layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
882         layoutParams.topMargin =
883                 includeTabsMargin
884                         ? getContext().getResources().getDimensionPixelSize(
885                         R.dimen.all_apps_header_pill_height)
886                         : 0;
887     }
888 
removeCustomRules(View v)889     private void removeCustomRules(View v) {
890         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
891             return;
892         }
893 
894         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
895         layoutParams.removeRule(RelativeLayout.ABOVE);
896         layoutParams.removeRule(RelativeLayout.ALIGN_TOP);
897         layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_TOP);
898     }
899 
createAdapter(AlphabeticalAppsList<T> appsList)900     protected BaseAllAppsAdapter<T> createAdapter(AlphabeticalAppsList<T> appsList) {
901         return new AllAppsGridAdapter<>(mActivityContext, getLayoutInflater(), appsList,
902                 mMainAdapterProvider);
903     }
904 
905     // TODO(b/216683257): Remove when Taskbar All Apps supports search.
isSearchSupported()906     protected boolean isSearchSupported() {
907         return true;
908     }
909 
layoutWithoutSearchContainer(View v, boolean includeTabsMargin)910     private void layoutWithoutSearchContainer(View v, boolean includeTabsMargin) {
911         if (!(v.getLayoutParams() instanceof RelativeLayout.LayoutParams)) {
912             return;
913         }
914 
915         RelativeLayout.LayoutParams layoutParams = (LayoutParams) v.getLayoutParams();
916         layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
917         layoutParams.topMargin = getContext().getResources().getDimensionPixelSize(includeTabsMargin
918                 ? R.dimen.all_apps_header_pill_height
919                 : R.dimen.all_apps_header_top_margin);
920     }
921 
isInAllApps()922     public boolean isInAllApps() {
923         // TODO: Make this abstract
924         return true;
925     }
926 
927     /**
928      * Inflates the search bar
929      */
inflateSearchBar()930     protected View inflateSearchBar() {
931         return mSearchUiDelegate.inflateSearchBar();
932     }
933 
934     /** The adapter provider for the main section. */
getMainAdapterProvider()935     public final SearchAdapterProvider<?> getMainAdapterProvider() {
936         return mMainAdapterProvider;
937     }
938 
939     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray)940     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) {
941         try {
942             // Many slice view id is not properly assigned, and hence throws null
943             // pointer exception in the underneath method. Catching the exception
944             // simply doesn't restore these slice views. This doesn't have any
945             // user visible effect because because we query them again.
946             super.dispatchRestoreInstanceState(sparseArray);
947         } catch (Exception e) {
948             Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e);
949         }
950 
951         Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null);
952         if (state != null) {
953             int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0);
954             if (currentPage == AdapterHolder.WORK && mViewPager != null) {
955                 mViewPager.setCurrentPage(currentPage);
956                 rebindAdapters();
957             } else {
958                 reset(true);
959             }
960         }
961     }
962 
963     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)964     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
965         super.dispatchSaveInstanceState(container);
966         Bundle state = new Bundle();
967         state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage());
968         container.put(R.id.work_tab_state_id, state);
969     }
970 
getAppsStore()971     public AllAppsStore<T> getAppsStore() {
972         return mAllAppsStore;
973     }
974 
getWorkManager()975     public WorkProfileManager getWorkManager() {
976         return mWorkManager;
977     }
978 
979     /** Returns whether Private Profile has been setup. */
hasPrivateProfile()980     public boolean hasPrivateProfile() {
981         return mHasPrivateApps;
982     }
983 
984     @Override
onDeviceProfileChanged(DeviceProfile dp)985     public void onDeviceProfileChanged(DeviceProfile dp) {
986         for (AdapterHolder holder : mAH) {
987             holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns);
988             holder.mAppsList.setNumAppsPerRowAllApps(dp.numShownAllAppsColumns);
989             if (holder.mRecyclerView != null) {
990                 // Remove all views and clear the pool, while keeping the data same. After this
991                 // call, all the viewHolders will be recreated.
992                 holder.mRecyclerView.swapAdapter(holder.mRecyclerView.getAdapter(), true);
993                 holder.mRecyclerView.getRecycledViewPool().clear();
994             }
995         }
996         updateBackgroundVisibility(dp);
997 
998         int navBarScrimColor = Themes.getNavBarScrimColor(mActivityContext);
999         if (mNavBarScrimPaint.getColor() != navBarScrimColor) {
1000             mNavBarScrimPaint.setColor(navBarScrimColor);
1001             invalidate();
1002         }
1003     }
1004 
updateBackgroundVisibility(DeviceProfile deviceProfile)1005     protected void updateBackgroundVisibility(DeviceProfile deviceProfile) {
1006         boolean visible = deviceProfile.isTablet || mForceBottomSheetVisible;
1007         mBottomSheetBackground.setVisibility(visible ? View.VISIBLE : View.GONE);
1008         // Note: For tablets, the opaque background and header protection are added in drawOnScrim.
1009         // For the taskbar entrypoint, the scrim is drawn by its abstract slide in view container,
1010         // so its header protection is derived from this scrim instead.
1011     }
1012 
setBottomSheetAlpha(float alpha)1013     private void setBottomSheetAlpha(float alpha) {
1014         // Bottom sheet alpha is always 1 for tablets.
1015         mBottomSheetAlpha = mActivityContext.getDeviceProfile().isTablet ? 1f : alpha;
1016     }
1017 
1018     @VisibleForTesting
onAppsUpdated()1019     public void onAppsUpdated() {
1020         mHasWorkApps = Stream.of(mAllAppsStore.getApps())
1021                 .anyMatch(mWorkManager.getItemInfoMatcher());
1022         mHasPrivateApps = Stream.of(mAllAppsStore.getApps())
1023                 .anyMatch(mPrivateProfileManager.getItemInfoMatcher());
1024         if (!isSearching()) {
1025             rebindAdapters();
1026         }
1027         if (mHasWorkApps) {
1028             mWorkManager.reset();
1029         }
1030         if (mHasPrivateApps) {
1031             mPrivateProfileManager.reset();
1032         }
1033 
1034         mActivityContext.getStatsLogManager().logger()
1035                 .withCardinality(mAllAppsStore.getApps().length)
1036                 .log(LAUNCHER_ALLAPPS_COUNT);
1037     }
1038 
1039     @Override
onInterceptTouchEvent(MotionEvent ev)1040     public boolean onInterceptTouchEvent(MotionEvent ev) {
1041         // The AllAppsContainerView houses the QSB and is hence visible from the Workspace
1042         // Overview states. We shouldn't intercept for the scrubber in these cases.
1043         if (!isInAllApps()) {
1044             mTouchHandler = null;
1045             return false;
1046         }
1047 
1048         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
1049             AllAppsRecyclerView rv = getActiveRecyclerView();
1050             if (rv != null && rv.getScrollbar() != null
1051                     && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) {
1052                 mTouchHandler = rv.getScrollbar();
1053             } else {
1054                 mTouchHandler = null;
1055             }
1056         }
1057         if (mTouchHandler != null) {
1058             return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
1059         }
1060         return false;
1061     }
1062 
1063     @Override
onTouchEvent(MotionEvent ev)1064     public boolean onTouchEvent(MotionEvent ev) {
1065         if (!isInAllApps()) {
1066             return false;
1067         }
1068 
1069         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
1070             AllAppsRecyclerView rv = getActiveRecyclerView();
1071             if (rv != null && rv.getScrollbar() != null
1072                     && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) {
1073                 mTouchHandler = rv.getScrollbar();
1074             } else {
1075                 mTouchHandler = null;
1076 
1077             }
1078         }
1079         if (mTouchHandler != null) {
1080             mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
1081             return true;
1082         }
1083         if (isSearching()
1084                 && mActivityContext.getDragLayer().isEventOverView(getVisibleContainerView(), ev)) {
1085             // if in search state, consume touch event.
1086             return true;
1087         }
1088         return false;
1089     }
1090 
1091     /** The current active recycler view (A-Z list from one of the profiles, or search results). */
getActiveRecyclerView()1092     public AllAppsRecyclerView getActiveRecyclerView() {
1093         if (isSearching()) {
1094             return getSearchRecyclerView();
1095         }
1096         return getActiveAppsRecyclerView();
1097     }
1098 
1099     /** The current focus change listener in the search container. */
getSearchFocusChangeListener()1100     public OnFocusChangeListener getSearchFocusChangeListener() {
1101         return mAH.get(AdapterHolder.SEARCH).mOnFocusChangeListener;
1102     }
1103 
1104     /** The current apps recycler view in the container. */
getActiveAppsRecyclerView()1105     private AllAppsRecyclerView getActiveAppsRecyclerView() {
1106         if (!mUsingTabs || isPersonalTab()) {
1107             return mAH.get(AdapterHolder.MAIN).mRecyclerView;
1108         } else {
1109             return mAH.get(AdapterHolder.WORK).mRecyclerView;
1110         }
1111     }
1112 
1113     /**
1114      * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently
1115      * hidden while searching.
1116      */
getAppsRecyclerViewContainer()1117     public ViewGroup getAppsRecyclerViewContainer() {
1118         return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
1119     }
1120 
1121     /** The RV for search results, which is hidden while A-Z apps are visible. */
getSearchRecyclerView()1122     public SearchRecyclerView getSearchRecyclerView() {
1123         return mSearchRecyclerView;
1124     }
1125 
isPersonalTab()1126     protected boolean isPersonalTab() {
1127         return mViewPager == null || mViewPager.getNextPage() == 0;
1128     }
1129 
1130     /**
1131      * Switches the current page to the provided {@code tab} if tabs are supported, otherwise does
1132      * nothing.
1133      */
switchToTab(int tab)1134     public void switchToTab(int tab) {
1135         if (mUsingTabs) {
1136             mViewPager.setCurrentPage(tab);
1137         }
1138     }
1139 
getLayoutInflater()1140     public LayoutInflater getLayoutInflater() {
1141         return mSearchUiDelegate.getLayoutInflater();
1142     }
1143 
1144     @Override
onDropCompleted(View target, DragObject d, boolean success)1145     public void onDropCompleted(View target, DragObject d, boolean success) {}
1146 
1147     @Override
setInsets(Rect insets)1148     public void setInsets(Rect insets) {
1149         mInsets.set(insets);
1150         DeviceProfile grid = mActivityContext.getDeviceProfile();
1151 
1152         applyAdapterSideAndBottomPaddings(grid);
1153 
1154         MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
1155         // Ignore left/right insets on tablet because we are already centered in-screen.
1156         if (grid.isTablet) {
1157             mlp.leftMargin = mlp.rightMargin = 0;
1158         } else {
1159             mlp.leftMargin = insets.left;
1160             mlp.rightMargin = insets.right;
1161         }
1162         setLayoutParams(mlp);
1163 
1164         if (!grid.isVerticalBarLayout() || FeatureFlags.enableResponsiveWorkspace()) {
1165             int topPadding = grid.allAppsPadding.top;
1166             if (isSearchBarFloating() && !grid.isTablet) {
1167                 topPadding += getResources().getDimensionPixelSize(
1168                         R.dimen.all_apps_additional_top_padding_floating_search);
1169             }
1170             setPadding(grid.allAppsLeftRightMargin, topPadding, grid.allAppsLeftRightMargin, 0);
1171         }
1172         InsettableFrameLayout.dispatchInsets(this, insets);
1173     }
1174 
1175     /**
1176      * Returns a padding in case a scrim is shown on the bottom of the view and a padding is needed.
1177      */
computeNavBarScrimHeight(WindowInsets insets)1178     protected int computeNavBarScrimHeight(WindowInsets insets) {
1179         return 0;
1180     }
1181 
1182     /**
1183      * Returns the current height of nav bar scrim
1184      */
getNavBarScrimHeight()1185     public int getNavBarScrimHeight() {
1186         return mNavBarScrimHeight;
1187     }
1188 
1189     @Override
dispatchApplyWindowInsets(WindowInsets insets)1190     public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
1191         mNavBarScrimHeight = computeNavBarScrimHeight(insets);
1192         applyAdapterSideAndBottomPaddings(mActivityContext.getDeviceProfile());
1193         return super.dispatchApplyWindowInsets(insets);
1194     }
1195 
1196     @Override
dispatchDraw(Canvas canvas)1197     protected void dispatchDraw(Canvas canvas) {
1198         super.dispatchDraw(canvas);
1199 
1200         if (mNavBarScrimHeight > 0) {
1201             canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
1202                     mNavBarScrimPaint);
1203         }
1204     }
1205 
updateSearchResultsVisibility()1206     protected void updateSearchResultsVisibility() {
1207         if (isSearching()) {
1208             getSearchRecyclerView().setVisibility(VISIBLE);
1209             getAppsRecyclerViewContainer().setVisibility(GONE);
1210             mHeader.setVisibility(GONE);
1211         } else {
1212             getSearchRecyclerView().setVisibility(GONE);
1213             getAppsRecyclerViewContainer().setVisibility(VISIBLE);
1214             mHeader.setVisibility(VISIBLE);
1215         }
1216         if (mHeader.isSetUp()) {
1217             mHeader.setActiveRV(getCurrentPage());
1218         }
1219     }
1220 
applyAdapterSideAndBottomPaddings(DeviceProfile grid)1221     private void applyAdapterSideAndBottomPaddings(DeviceProfile grid) {
1222         int bottomPadding = Math.max(mInsets.bottom, mNavBarScrimHeight);
1223         mAH.forEach(adapterHolder -> {
1224             adapterHolder.mPadding.bottom = bottomPadding;
1225             adapterHolder.mPadding.left = grid.allAppsPadding.left;
1226             adapterHolder.mPadding.right = grid.allAppsPadding.right;
1227             adapterHolder.applyPadding();
1228         });
1229     }
1230 
setDeviceManagementResources()1231     private void setDeviceManagementResources() {
1232         if (mActivityContext.getStringCache() != null) {
1233             Button personalTab = findViewById(R.id.tab_personal);
1234             personalTab.setText(mActivityContext.getStringCache().allAppsPersonalTab);
1235 
1236             Button workTab = findViewById(R.id.tab_work);
1237             workTab.setText(mActivityContext.getStringCache().allAppsWorkTab);
1238         }
1239     }
1240 
1241     /**
1242      * Returns true if the container has work apps.
1243      */
shouldShowTabs()1244     public boolean shouldShowTabs() {
1245         return mHasWorkApps;
1246     }
1247 
1248     // Used by tests only
isDescendantViewVisible(int viewId)1249     private boolean isDescendantViewVisible(int viewId) {
1250         final View view = findViewById(viewId);
1251         if (view == null) return false;
1252 
1253         if (!view.isShown()) return false;
1254 
1255         return view.getGlobalVisibleRect(new Rect());
1256     }
1257 
1258     /** Called in Launcher#bindStringCache() to update the UI when cache is updated. */
updateWorkUI()1259     public void updateWorkUI() {
1260         setDeviceManagementResources();
1261         if (mWorkManager.getWorkModeSwitch() != null) {
1262             mWorkManager.getWorkModeSwitch().updateStringFromCache();
1263         }
1264         inflateWorkCardsIfNeeded();
1265     }
1266 
inflateWorkCardsIfNeeded()1267     private void inflateWorkCardsIfNeeded() {
1268         AllAppsRecyclerView workRV = mAH.get(AdapterHolder.WORK).mRecyclerView;
1269         if (workRV != null) {
1270             for (int i = 0; i < workRV.getChildCount(); i++) {
1271                 View currentView  = workRV.getChildAt(i);
1272                 int currentItemViewType = workRV.getChildViewHolder(currentView).getItemViewType();
1273                 if (currentItemViewType == VIEW_TYPE_WORK_EDU_CARD) {
1274                     ((WorkEduCard) currentView).updateStringFromCache();
1275                 } else if (currentItemViewType == VIEW_TYPE_WORK_DISABLED_CARD) {
1276                     ((WorkPausedCard) currentView).updateStringFromCache();
1277                 }
1278             }
1279         }
1280     }
1281 
1282     @VisibleForTesting
setWorkManager(WorkProfileManager workManager)1283     public void setWorkManager(WorkProfileManager workManager) {
1284         mWorkManager = workManager;
1285     }
1286 
1287     @VisibleForTesting
isPersonalTabVisible()1288     public boolean isPersonalTabVisible() {
1289         return isDescendantViewVisible(R.id.tab_personal);
1290     }
1291 
1292     @VisibleForTesting
isWorkTabVisible()1293     public boolean isWorkTabVisible() {
1294         return isDescendantViewVisible(R.id.tab_work);
1295     }
1296 
getSearchResultList()1297     public AlphabeticalAppsList<T> getSearchResultList() {
1298         return mAH.get(SEARCH).mAppsList;
1299     }
1300 
getPersonalAppList()1301     public AlphabeticalAppsList<T> getPersonalAppList() {
1302         return mAH.get(MAIN).mAppsList;
1303     }
1304 
getFloatingHeaderView()1305     public FloatingHeaderView getFloatingHeaderView() {
1306         return mHeader;
1307     }
1308 
1309     @VisibleForTesting
getContentView()1310     public View getContentView() {
1311         return isSearching() ? getSearchRecyclerView() : getAppsRecyclerViewContainer();
1312     }
1313 
1314     /** The current page visible in all apps. */
getCurrentPage()1315     public int getCurrentPage() {
1316         return isSearching()
1317                 ? SEARCH
1318                 : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage();
1319     }
1320 
getPrivateProfileManager()1321     public PrivateProfileManager getPrivateProfileManager() {
1322         return mPrivateProfileManager;
1323     }
1324 
1325     /**
1326      * Adds an update listener to animator that adds springs to the animation.
1327      */
addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity , float progress )1328     public void addSpringFromFlingUpdateListener(ValueAnimator animator,
1329             float velocity /* release velocity */,
1330             float progress /* portion of the distance to travel*/) {
1331         animator.addListener(new AnimatorListenerAdapter() {
1332             @Override
1333             public void onAnimationStart(Animator animator) {
1334                 float distance = (1 - progress) * getHeight(); // px
1335                 float settleVelocity = Math.min(0, distance
1336                         / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration())
1337                         + velocity);
1338                 absorbSwipeUpVelocity(Math.max(1000, Math.abs(
1339                         Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER))));
1340             }
1341         });
1342     }
1343 
1344     /** Invoked when the container is pulled. */
onPull(float deltaDistance, float displacement)1345     public void onPull(float deltaDistance, float displacement) {
1346         absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement);
1347         // Current motion spec is to actually push and not pull
1348         // on this surface. However, until EdgeEffect.onPush (b/190612804) is
1349         // implemented at view level, we will simply pull
1350     }
1351 
1352     @Override
getDrawingRect(Rect outRect)1353     public void getDrawingRect(Rect outRect) {
1354         super.getDrawingRect(outRect);
1355         outRect.offset(0, (int) getTranslationY());
1356     }
1357 
1358     @Override
setTranslationY(float translationY)1359     public void setTranslationY(float translationY) {
1360         super.setTranslationY(translationY);
1361         invalidateHeader();
1362     }
1363 
1364     /**
1365      * Set {@link Animator.AnimatorListener} on {@link mAllAppsTransitionController} to observe
1366      * animation of backing out of all apps search view to all apps view.
1367      */
setAllAppsSearchBackAnimatorListener(Animator.AnimatorListener listener)1368     public void setAllAppsSearchBackAnimatorListener(Animator.AnimatorListener listener) {
1369         Preconditions.assertNotNull(mAllAppsTransitionController);
1370         if (mAllAppsTransitionController == null) {
1371             return;
1372         }
1373         mAllAppsTransitionController.setAllAppsSearchBackAnimationListener(listener);
1374     }
1375 
setScrimView(ScrimView scrimView)1376     public void setScrimView(ScrimView scrimView) {
1377         mScrimView = scrimView;
1378     }
1379 
1380     @Override
drawOnScrimWithScaleAndBottomOffset( Canvas canvas, float scale, @Px int bottomOffsetPx)1381     public void drawOnScrimWithScaleAndBottomOffset(
1382             Canvas canvas, float scale, @Px int bottomOffsetPx) {
1383         final View panel = mBottomSheetBackground;
1384         final boolean hasBottomSheet = panel.getVisibility() == VISIBLE;
1385         final float translationY = ((View) panel.getParent()).getTranslationY();
1386 
1387         final float horizontalScaleOffset = (1 - scale) * panel.getWidth() / 2;
1388         final float verticalScaleOffset = (1 - scale) * (panel.getHeight() - getHeight() / 2);
1389 
1390         final float topNoScale = panel.getTop() + translationY;
1391         final float topWithScale = topNoScale + verticalScaleOffset;
1392         final float leftWithScale = panel.getLeft() + horizontalScaleOffset;
1393         final float rightWithScale = panel.getRight() - horizontalScaleOffset;
1394         final float bottomWithOffset = panel.getBottom() + bottomOffsetPx;
1395         // Draw full background panel for tablets.
1396         if (hasBottomSheet) {
1397             mHeaderPaint.setColor(mBottomSheetBackgroundColor);
1398             mHeaderPaint.setAlpha((int) (255 * mBottomSheetAlpha));
1399 
1400             mTmpRectF.set(
1401                     leftWithScale,
1402                     topWithScale,
1403                     rightWithScale,
1404                     bottomWithOffset);
1405             mTmpPath.reset();
1406             mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW);
1407             canvas.drawPath(mTmpPath, mHeaderPaint);
1408         }
1409 
1410         if (DEBUG_HEADER_PROTECTION) {
1411             mHeaderPaint.setColor(Color.MAGENTA);
1412             mHeaderPaint.setAlpha(255);
1413         } else {
1414             mHeaderPaint.setColor(mHeaderColor);
1415             mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor)));
1416         }
1417         if (mHeaderPaint.getColor() == mScrimColor || mHeaderPaint.getColor() == 0) {
1418             return;
1419         }
1420 
1421         // Draw header on background panel
1422         final float headerBottomNoScale =
1423                 getHeaderBottom() + getVisibleContainerView().getPaddingTop();
1424         final float headerHeightNoScale = headerBottomNoScale - topNoScale;
1425         final float headerBottomWithScaleOnTablet = topWithScale + headerHeightNoScale * scale;
1426         final float headerBottomOffset = (getVisibleContainerView().getHeight() * (1 - scale) / 2);
1427         final float headerBottomWithScaleOnPhone = headerBottomNoScale * scale + headerBottomOffset;
1428         final FloatingHeaderView headerView = getFloatingHeaderView();
1429         if (hasBottomSheet) {
1430             // Start adding header protection if search bar or tabs will attach to the top.
1431             if (!isSearchBarFloating() || mUsingTabs) {
1432                 mTmpRectF.set(
1433                         leftWithScale,
1434                         topWithScale,
1435                         rightWithScale,
1436                         headerBottomWithScaleOnTablet);
1437                 mTmpPath.reset();
1438                 mTmpPath.addRoundRect(mTmpRectF, mBottomSheetCornerRadii, Direction.CW);
1439                 canvas.drawPath(mTmpPath, mHeaderPaint);
1440             }
1441         } else {
1442             canvas.drawRect(0, 0, canvas.getWidth(), headerBottomWithScaleOnPhone, mHeaderPaint);
1443         }
1444 
1445         // If tab exist (such as work profile), extend header with tab height
1446         final int tabsHeight = headerView.getPeripheralProtectionHeight(/* expectedHeight */ false);
1447         if (mTabsProtectionAlpha > 0 && tabsHeight != 0) {
1448             if (DEBUG_HEADER_PROTECTION) {
1449                 mHeaderPaint.setColor(Color.BLUE);
1450                 mHeaderPaint.setAlpha(255);
1451             } else {
1452                 mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha));
1453             }
1454             float left = 0f;
1455             float right = canvas.getWidth();
1456             if (hasBottomSheet) {
1457                 left = mBottomSheetBackground.getLeft() + horizontalScaleOffset;
1458                 right = mBottomSheetBackground.getRight() - horizontalScaleOffset;
1459             }
1460 
1461             final float tabTopWithScale = hasBottomSheet
1462                     ? headerBottomWithScaleOnTablet
1463                     : headerBottomWithScaleOnPhone;
1464             final float tabBottomWithScale = tabTopWithScale + tabsHeight * scale;
1465 
1466             canvas.drawRect(
1467                     left,
1468                     tabTopWithScale,
1469                     right,
1470                     tabBottomWithScale,
1471                     mHeaderPaint);
1472         }
1473     }
1474 
1475     /**
1476      * The height of the header protection as if the user scrolled down the app list.
1477      */
getHeaderProtectionHeight()1478     float getHeaderProtectionHeight() {
1479         float headerBottom = getHeaderBottom() - getTranslationY();
1480         if (mUsingTabs) {
1481             return headerBottom + mHeader.getPeripheralProtectionHeight(/* expectedHeight */ true);
1482         } else {
1483             return headerBottom;
1484         }
1485     }
1486 
1487     /**
1488      * redraws header protection
1489      */
invalidateHeader()1490     public void invalidateHeader() {
1491         if (mScrimView != null) {
1492             mScrimView.invalidate();
1493         }
1494     }
1495 
1496     /** Returns the position of the bottom edge of the header */
getHeaderBottom()1497     public int getHeaderBottom() {
1498         int bottom = (int) getTranslationY() + mHeader.getClipTop();
1499         if (isSearchBarFloating()) {
1500             if (mActivityContext.getDeviceProfile().isTablet) {
1501                 return bottom + mBottomSheetBackground.getTop();
1502             }
1503             return bottom;
1504         }
1505         return bottom + mHeader.getTop();
1506     }
1507 
isUsingTabs()1508     boolean isUsingTabs() {
1509         return mUsingTabs;
1510     }
1511 
1512     /**
1513      * Returns a view that denotes the visible part of all apps container view.
1514      */
getVisibleContainerView()1515     public View getVisibleContainerView() {
1516         return mBottomSheetBackground.getVisibility() == VISIBLE ? mBottomSheetBackground : this;
1517     }
1518 
onInitializeRecyclerView(RecyclerView rv)1519     protected void onInitializeRecyclerView(RecyclerView rv) {
1520         rv.addOnScrollListener(mScrollListener);
1521         mSearchUiDelegate.onInitializeRecyclerView(rv);
1522     }
1523 
1524     /** Returns the instance of @{code SearchTransitionController}. */
getSearchTransitionController()1525     public SearchTransitionController getSearchTransitionController() {
1526         return mSearchTransitionController;
1527     }
1528 
1529     /** Holds a {@link BaseAllAppsAdapter} and related fields. */
1530     public class AdapterHolder {
1531         public static final int MAIN = 0;
1532         public static final int WORK = 1;
1533         public static final int SEARCH = 2;
1534 
1535         private final int mType;
1536         public final BaseAllAppsAdapter<T> mAdapter;
1537         final RecyclerView.LayoutManager mLayoutManager;
1538         final AlphabeticalAppsList<T> mAppsList;
1539         final Rect mPadding = new Rect();
1540         AllAppsRecyclerView mRecyclerView;
1541         private OnFocusChangeListener mOnFocusChangeListener;
1542 
AdapterHolder(int type, AlphabeticalAppsList<T> appsList)1543         AdapterHolder(int type, AlphabeticalAppsList<T> appsList) {
1544             mType = type;
1545             mAppsList = appsList;
1546             mAdapter = createAdapter(mAppsList);
1547             mAppsList.setAdapter(mAdapter);
1548             mLayoutManager = mAdapter.getLayoutManager();
1549         }
1550 
setup(@onNull View rv, @Nullable Predicate<ItemInfo> matcher)1551         void setup(@NonNull View rv, @Nullable Predicate<ItemInfo> matcher) {
1552             mAppsList.updateItemFilter(matcher);
1553             mRecyclerView = (AllAppsRecyclerView) rv;
1554             mRecyclerView.bindFastScrollbar(mFastScroller);
1555             mRecyclerView.setEdgeEffectFactory(createEdgeEffectFactory());
1556             mRecyclerView.setApps(mAppsList);
1557             mRecyclerView.setLayoutManager(mLayoutManager);
1558             mRecyclerView.setAdapter(mAdapter);
1559             mRecyclerView.setHasFixedSize(true);
1560             // No animations will occur when changes occur to the items in this RecyclerView.
1561             mRecyclerView.setItemAnimator(null);
1562             onInitializeRecyclerView(mRecyclerView);
1563             // Use ViewGroupFocusHelper for SearchRecyclerView to draw focus outline for the
1564             // buttons in the view (e.g. query builder button and setting button)
1565             FocusedItemDecorator focusedItemDecorator = isSearch() ? new FocusedItemDecorator(
1566                     new ViewGroupFocusHelper(mRecyclerView)) : new FocusedItemDecorator(
1567                     mRecyclerView);
1568             mRecyclerView.addItemDecoration(focusedItemDecorator);
1569             mOnFocusChangeListener = focusedItemDecorator.getFocusListener();
1570             mAdapter.setIconFocusListener(mOnFocusChangeListener);
1571             applyPadding();
1572         }
1573 
applyPadding()1574         void applyPadding() {
1575             if (mRecyclerView != null) {
1576                 int bottomOffset = 0;
1577                 if (isWork() && mWorkManager.getWorkModeSwitch() != null) {
1578                     bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
1579                 } else if (isMain() && mPrivateProfileManager != null) {
1580                     Optional<AdapterItem> privateSpaceHeaderItem = mAppsList.getAdapterItems()
1581                             .stream()
1582                             .filter(item -> item.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER)
1583                             .findFirst();
1584                     if (privateSpaceHeaderItem.isPresent()) {
1585                         bottomOffset = mPrivateSpaceBottomExtraSpace;
1586                     }
1587                 }
1588                 if (isSearchBarFloating()) {
1589                     bottomOffset += mSearchContainer.getHeight();
1590                 }
1591                 mRecyclerView.setPadding(mPadding.left, mPadding.top, mPadding.right,
1592                         mPadding.bottom + bottomOffset);
1593             }
1594         }
1595 
isWork()1596         private boolean isWork() {
1597             return mType == WORK;
1598         }
1599 
isSearch()1600         private boolean isSearch() {
1601             return mType == SEARCH;
1602         }
1603 
isMain()1604         private boolean isMain() {
1605             return mType == MAIN;
1606         }
1607     }
1608 }
1609