1 /*
2  * Copyright (C) 2015 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.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ValueAnimator;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.graphics.Point;
29 import android.graphics.Rect;
30 import android.os.Bundle;
31 import android.os.Parcelable;
32 import android.os.Process;
33 import android.os.UserManager;
34 import android.text.SpannableStringBuilder;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.util.SparseArray;
38 import android.view.KeyEvent;
39 import android.view.LayoutInflater;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.WindowInsets;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.Nullable;
47 import androidx.annotation.StringRes;
48 import androidx.annotation.VisibleForTesting;
49 import androidx.core.graphics.ColorUtils;
50 import androidx.recyclerview.widget.LinearLayoutManager;
51 import androidx.recyclerview.widget.RecyclerView;
52 
53 import com.android.launcher3.BaseDraggingActivity;
54 import com.android.launcher3.DeviceProfile;
55 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
56 import com.android.launcher3.DragSource;
57 import com.android.launcher3.DropTarget.DragObject;
58 import com.android.launcher3.Insettable;
59 import com.android.launcher3.InsettableFrameLayout;
60 import com.android.launcher3.R;
61 import com.android.launcher3.Utilities;
62 import com.android.launcher3.allapps.search.SearchAdapterProvider;
63 import com.android.launcher3.config.FeatureFlags;
64 import com.android.launcher3.keyboard.FocusedItemDecorator;
65 import com.android.launcher3.model.data.AppInfo;
66 import com.android.launcher3.util.ItemInfoMatcher;
67 import com.android.launcher3.util.Themes;
68 import com.android.launcher3.views.RecyclerViewFastScroller;
69 import com.android.launcher3.views.ScrimView;
70 import com.android.launcher3.views.SpringRelativeLayout;
71 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
72 
73 /**
74  * The all apps view container.
75  */
76 public class AllAppsContainerView extends SpringRelativeLayout implements DragSource,
77         Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener,
78         ScrimView.ScrimDrawingController {
79 
80     private static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
81 
82     public static final float PULL_MULTIPLIER = .02f;
83     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
84 
85     private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
86     private final Rect mInsets = new Rect();
87 
88     protected final BaseDraggingActivity mLauncher;
89     protected final AdapterHolder[] mAH;
90     protected final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(
91             Process.myUserHandle());
92     private final AllAppsStore mAllAppsStore = new AllAppsStore();
93 
94     private final RecyclerView.OnScrollListener mScrollListener =
95             new RecyclerView.OnScrollListener() {
96                 @Override
97                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
98                     updateHeaderScroll(((AllAppsRecyclerView) recyclerView).getCurrentScrollY());
99                 }
100             };
101     private final WorkProfileManager mWorkManager;
102 
103 
104     private final Paint mNavBarScrimPaint;
105     private int mNavBarScrimHeight = 0;
106 
107     protected SearchUiManager mSearchUiManager;
108     private View mSearchContainer;
109     private AllAppsPagedView mViewPager;
110 
111     protected FloatingHeaderView mHeader;
112 
113 
114     private SpannableStringBuilder mSearchQueryBuilder = null;
115 
116     protected boolean mUsingTabs;
117     private boolean mIsSearching;
118     private boolean mHasWorkApps;
119 
120     protected RecyclerViewFastScroller mTouchHandler;
121     protected final Point mFastScrollerOffset = new Point();
122 
123     private SearchAdapterProvider mSearchAdapterProvider;
124     private final int mScrimColor;
125     private final int mHeaderProtectionColor;
126     private final float mHeaderThreshold;
127     private ScrimView mScrimView;
128     private int mHeaderColor;
129     private int mTabsProtectionAlpha;
130 
AllAppsContainerView(Context context)131     public AllAppsContainerView(Context context) {
132         this(context, null);
133     }
134 
AllAppsContainerView(Context context, AttributeSet attrs)135     public AllAppsContainerView(Context context, AttributeSet attrs) {
136         this(context, attrs, 0);
137     }
138 
AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr)139     public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
140         super(context, attrs, defStyleAttr);
141 
142         mLauncher = BaseDraggingActivity.fromContext(context);
143 
144         mScrimColor = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
145         mHeaderThreshold = getResources().getDimensionPixelSize(
146                 R.dimen.dynamic_grid_cell_border_spacing);
147         mHeaderProtectionColor = Themes.getAttrColor(context, R.attr.allappsHeaderProtectionColor);
148 
149         mLauncher.addOnDeviceProfileChangeListener(this);
150 
151         mSearchAdapterProvider = mLauncher.createSearchAdapterProvider(this);
152 
153         mAH = new AdapterHolder[2];
154 
155         mWorkManager = new WorkProfileManager(mLauncher.getSystemService(UserManager.class), this,
156                 Utilities.getPrefs(mLauncher));
157         mAH[AdapterHolder.MAIN] = new AdapterHolder(false /* isWork */);
158         mAH[AdapterHolder.WORK] = new AdapterHolder(true /* isWork */);
159 
160         mNavBarScrimPaint = new Paint();
161         mNavBarScrimPaint.setColor(Themes.getAttrColor(context, R.attr.allAppsNavBarScrimColor));
162 
163         mAllAppsStore.addUpdateListener(this::onAppsUpdated);
164     }
165 
166     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray)167     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> sparseArray) {
168         try {
169             // Many slice view id is not properly assigned, and hence throws null
170             // pointer exception in the underneath method. Catching the exception
171             // simply doesn't restore these slice views. This doesn't have any
172             // user visible effect because because we query them again.
173             super.dispatchRestoreInstanceState(sparseArray);
174         } catch (Exception e) {
175             Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e);
176         }
177 
178         Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null);
179         if (state != null) {
180             int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0);
181             if (currentPage != 0 && mViewPager != null) {
182                 mViewPager.setCurrentPage(currentPage);
183                 rebindAdapters();
184             } else {
185                 reset(true);
186             }
187         }
188 
189     }
190 
191     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)192     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
193         super.dispatchSaveInstanceState(container);
194         Bundle state = new Bundle();
195         state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage());
196         container.put(R.id.work_tab_state_id, state);
197     }
198 
199     /**
200      * Sets the long click listener for icons
201      */
setOnIconLongClickListener(OnLongClickListener listener)202     public void setOnIconLongClickListener(OnLongClickListener listener) {
203         for (AdapterHolder holder : mAH) {
204             holder.adapter.setOnIconLongClickListener(listener);
205         }
206     }
207 
getAppsStore()208     public AllAppsStore getAppsStore() {
209         return mAllAppsStore;
210     }
211 
getWorkManager()212     public WorkProfileManager getWorkManager() {
213         return mWorkManager;
214     }
215 
216     @Override
onDeviceProfileChanged(DeviceProfile dp)217     public void onDeviceProfileChanged(DeviceProfile dp) {
218         for (AdapterHolder holder : mAH) {
219             holder.adapter.setAppsPerRow(dp.numShownAllAppsColumns);
220             if (holder.recyclerView != null) {
221                 // Remove all views and clear the pool, while keeping the data same. After this
222                 // call, all the viewHolders will be recreated.
223                 holder.recyclerView.swapAdapter(holder.recyclerView.getAdapter(), true);
224                 holder.recyclerView.getRecycledViewPool().clear();
225             }
226         }
227     }
228 
onAppsUpdated()229     private void onAppsUpdated() {
230         boolean hasWorkApps = false;
231         for (AppInfo app : mAllAppsStore.getApps()) {
232             if (mWorkManager.getMatcher().matches(app, null)) {
233                 hasWorkApps = true;
234                 break;
235             }
236         }
237         mHasWorkApps = hasWorkApps;
238         if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) {
239             rebindAdapters();
240             if (hasWorkApps) {
241                 mWorkManager.reset();
242             }
243         }
244     }
245 
246     /**
247      * Returns whether the view itself will handle the touch event or not.
248      */
shouldContainerScroll(MotionEvent ev)249     public boolean shouldContainerScroll(MotionEvent ev) {
250         // IF the MotionEvent is inside the search box, and the container keeps on receiving
251         // touch input, container should move down.
252         if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) {
253             return true;
254         }
255         AllAppsRecyclerView rv = getActiveRecyclerView();
256         if (rv == null) {
257             return true;
258         }
259         if (rv.getScrollbar().getThumbOffsetY() >= 0 &&
260                 mLauncher.getDragLayer().isEventOverView(rv.getScrollbar(), ev)) {
261             return false;
262         }
263         return rv.shouldContainerScroll(ev, mLauncher.getDragLayer());
264     }
265 
266     @Override
onInterceptTouchEvent(MotionEvent ev)267     public boolean onInterceptTouchEvent(MotionEvent ev) {
268         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
269             AllAppsRecyclerView rv = getActiveRecyclerView();
270             if (rv != null &&
271                     rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(), mFastScrollerOffset)) {
272                 mTouchHandler = rv.getScrollbar();
273             } else {
274                 mTouchHandler = null;
275             }
276         }
277         if (mTouchHandler != null) {
278             return mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
279         }
280         return false;
281     }
282 
283     @Override
onTouchEvent(MotionEvent ev)284     public boolean onTouchEvent(MotionEvent ev) {
285         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
286             AllAppsRecyclerView rv = getActiveRecyclerView();
287             if (rv != null && rv.getScrollbar().isHitInParent(ev.getX(), ev.getY(),
288                     mFastScrollerOffset)) {
289                 mTouchHandler = rv.getScrollbar();
290             } else {
291                 mTouchHandler = null;
292 
293             }
294         }
295         if (mTouchHandler != null) {
296             mTouchHandler.handleTouchEvent(ev, mFastScrollerOffset);
297             return true;
298         }
299         return false;
300     }
301 
getDescription()302     public String getDescription() {
303         @StringRes int descriptionRes;
304         if (mUsingTabs) {
305             descriptionRes =
306                     mViewPager.getNextPage() == 0
307                             ? R.string.all_apps_button_personal_label
308                             : R.string.all_apps_button_work_label;
309         } else if (mIsSearching) {
310             descriptionRes = R.string.all_apps_search_results;
311         } else {
312             descriptionRes = R.string.all_apps_button_label;
313         }
314         return getContext().getString(descriptionRes);
315     }
316 
getActiveRecyclerView()317     public AllAppsRecyclerView getActiveRecyclerView() {
318         if (!mUsingTabs || mViewPager.getNextPage() == 0) {
319             return mAH[AdapterHolder.MAIN].recyclerView;
320         } else {
321             return mAH[AdapterHolder.WORK].recyclerView;
322         }
323     }
324 
getLayoutInflater()325     public LayoutInflater getLayoutInflater() {
326         return LayoutInflater.from(getContext());
327     }
328 
329     /**
330      * Resets the state of AllApps.
331      */
reset(boolean animate)332     public void reset(boolean animate) {
333         for (int i = 0; i < mAH.length; i++) {
334             if (mAH[i].recyclerView != null) {
335                 mAH[i].recyclerView.scrollToTop();
336             }
337         }
338         if (isHeaderVisible()) {
339             mHeader.reset(animate);
340         }
341         // Reset the search bar and base recycler view after transitioning home
342         mSearchUiManager.resetSearch();
343         updateHeaderScroll(0);
344     }
345 
346     @Override
onFinishInflate()347     protected void onFinishInflate() {
348         super.onFinishInflate();
349 
350         // This is a focus listener that proxies focus from a view into the list view.  This is to
351         // work around the search box from getting first focus and showing the cursor.
352         setOnFocusChangeListener((v, hasFocus) -> {
353             if (hasFocus && getActiveRecyclerView() != null) {
354                 getActiveRecyclerView().requestFocus();
355             }
356         });
357 
358         mHeader = findViewById(R.id.all_apps_header);
359         rebindAdapters(true /* force */);
360 
361         mSearchContainer = findViewById(R.id.search_container_all_apps);
362         mSearchUiManager = (SearchUiManager) mSearchContainer;
363         mSearchUiManager.initializeSearch(this);
364     }
365 
getSearchUiManager()366     public SearchUiManager getSearchUiManager() {
367         return mSearchUiManager;
368     }
369 
370     @Override
dispatchKeyEvent(KeyEvent event)371     public boolean dispatchKeyEvent(KeyEvent event) {
372         mSearchUiManager.preDispatchKeyEvent(event);
373         return super.dispatchKeyEvent(event);
374     }
375 
376     @Override
onDropCompleted(View target, DragObject d, boolean success)377     public void onDropCompleted(View target, DragObject d, boolean success) {
378     }
379 
380     @Override
setInsets(Rect insets)381     public void setInsets(Rect insets) {
382         mInsets.set(insets);
383         DeviceProfile grid = mLauncher.getDeviceProfile();
384 
385         for (int i = 0; i < mAH.length; i++) {
386             mAH[i].padding.bottom = insets.bottom;
387             mAH[i].padding.left = mAH[i].padding.right = grid.allAppsLeftRightPadding;
388             mAH[i].applyPadding();
389         }
390 
391         ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
392         mlp.leftMargin = insets.left;
393         mlp.rightMargin = insets.right;
394         setLayoutParams(mlp);
395 
396         if (grid.isVerticalBarLayout()) {
397             setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
398         } else {
399             setPadding(0, 0, 0, 0);
400         }
401 
402         InsettableFrameLayout.dispatchInsets(this, insets);
403     }
404 
405     @Override
dispatchApplyWindowInsets(WindowInsets insets)406     public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
407         if (Utilities.ATLEAST_Q) {
408             mNavBarScrimHeight = insets.getTappableElementInsets().bottom;
409         } else {
410             mNavBarScrimHeight = insets.getStableInsetBottom();
411         }
412         return super.dispatchApplyWindowInsets(insets);
413     }
414 
415     @Override
dispatchDraw(Canvas canvas)416     protected void dispatchDraw(Canvas canvas) {
417         super.dispatchDraw(canvas);
418 
419         if (mNavBarScrimHeight > 0) {
420             canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
421                     mNavBarScrimPaint);
422         }
423     }
424 
rebindAdapters()425     private void rebindAdapters() {
426         rebindAdapters(false /* force */);
427     }
428 
rebindAdapters(boolean force)429     protected void rebindAdapters(boolean force) {
430         boolean showTabs = mHasWorkApps && !mIsSearching;
431         if (showTabs == mUsingTabs && !force) {
432             return;
433         }
434         replaceRVContainer(showTabs);
435         mUsingTabs = showTabs;
436 
437         mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.MAIN].recyclerView);
438         mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.WORK].recyclerView);
439 
440         if (mUsingTabs) {
441             mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher);
442             mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
443             mAH[AdapterHolder.WORK].recyclerView.setId(R.id.apps_list_view_work);
444             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
445             findViewById(R.id.tab_personal)
446                     .setOnClickListener((View view) -> {
447                         if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
448                             mLauncher.getStatsLogManager().logger()
449                                     .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
450                         }
451                     });
452             findViewById(R.id.tab_work)
453                     .setOnClickListener((View view) -> {
454                         if (mViewPager.snapToPage(AdapterHolder.WORK)) {
455                             mLauncher.getStatsLogManager().logger()
456                                     .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
457                         }
458                     });
459             onActivePageChanged(mViewPager.getNextPage());
460         } else {
461             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
462             mAH[AdapterHolder.WORK].recyclerView = null;
463         }
464         setupHeader();
465 
466         mAllAppsStore.registerIconContainer(mAH[AdapterHolder.MAIN].recyclerView);
467         mAllAppsStore.registerIconContainer(mAH[AdapterHolder.WORK].recyclerView);
468     }
469 
470 
replaceRVContainer(boolean showTabs)471     private void replaceRVContainer(boolean showTabs) {
472         for (AdapterHolder adapterHolder : mAH) {
473             if (adapterHolder.recyclerView != null) {
474                 adapterHolder.recyclerView.setLayoutManager(null);
475                 adapterHolder.recyclerView.setAdapter(null);
476             }
477         }
478         View oldView = getRecyclerViewContainer();
479         int index = indexOfChild(oldView);
480         removeView(oldView);
481         int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
482         View newView = getLayoutInflater().inflate(layout, this, false);
483         addView(newView, index);
484         if (showTabs) {
485             mViewPager = (AllAppsPagedView) newView;
486             mViewPager.initParentViews(this);
487             mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
488             if (mWorkManager.attachWorkModeSwitch()) {
489                 mWorkManager.getWorkModeSwitch().post(() -> mAH[AdapterHolder.WORK].applyPadding());
490             }
491         } else {
492             mWorkManager.detachWorkModeSwitch();
493             mViewPager = null;
494         }
495     }
496 
getRecyclerViewContainer()497     public View getRecyclerViewContainer() {
498         return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
499     }
500 
501     @Override
onActivePageChanged(int currentActivePage)502     public void onActivePageChanged(int currentActivePage) {
503         mHeader.setMainActive(currentActivePage == AdapterHolder.MAIN);
504         if (mAH[currentActivePage].recyclerView != null) {
505             mAH[currentActivePage].recyclerView.bindFastScrollbar();
506         }
507         reset(true /* animate */);
508 
509         mWorkManager.onActivePageChanged(currentActivePage);
510     }
511 
512     // Used by tests only
isDescendantViewVisible(int viewId)513     private boolean isDescendantViewVisible(int viewId) {
514         final View view = findViewById(viewId);
515         if (view == null) return false;
516 
517         if (!view.isShown()) return false;
518 
519         return view.getGlobalVisibleRect(new Rect());
520     }
521 
522     @VisibleForTesting
isPersonalTabVisible()523     public boolean isPersonalTabVisible() {
524         return isDescendantViewVisible(R.id.tab_personal);
525     }
526 
527     // Used by tests only
isWorkTabVisible()528     public boolean isWorkTabVisible() {
529         return isDescendantViewVisible(R.id.tab_work);
530     }
531 
getApps()532     public AlphabeticalAppsList getApps() {
533         return mAH[AdapterHolder.MAIN].appsList;
534     }
535 
getFloatingHeaderView()536     public FloatingHeaderView getFloatingHeaderView() {
537         return mHeader;
538     }
539 
getSearchView()540     public View getSearchView() {
541         return mSearchContainer;
542     }
543 
getContentView()544     public View getContentView() {
545         return mViewPager == null ? getActiveRecyclerView() : mViewPager;
546     }
547 
getCurrentPage()548     public int getCurrentPage() {
549         return mViewPager != null ? mViewPager.getCurrentPage() : AdapterHolder.MAIN;
550     }
551 
552     /**
553      * Handles selection on focused view and returns success
554      */
launchHighlightedItem()555     public boolean launchHighlightedItem() {
556         if (mSearchAdapterProvider == null) return false;
557         return mSearchAdapterProvider.launchHighlightedItem();
558     }
559 
getSearchAdapterProvider()560     public SearchAdapterProvider getSearchAdapterProvider() {
561         return mSearchAdapterProvider;
562     }
563 
getScrollBar()564     public RecyclerViewFastScroller getScrollBar() {
565         AllAppsRecyclerView rv = getActiveRecyclerView();
566         return rv == null ? null : rv.getScrollbar();
567     }
568 
setupHeader()569     public void setupHeader() {
570         mHeader.setVisibility(View.VISIBLE);
571         mHeader.setup(mAH, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null);
572 
573         int padding = mHeader.getMaxTranslation();
574         for (int i = 0; i < mAH.length; i++) {
575             mAH[i].padding.top = padding;
576             mAH[i].applyPadding();
577             if (mAH[i].recyclerView != null) {
578                 mAH[i].recyclerView.scrollToTop();
579             }
580         }
581     }
582 
setLastSearchQuery(String query)583     public void setLastSearchQuery(String query) {
584         for (int i = 0; i < mAH.length; i++) {
585             mAH[i].adapter.setLastSearchQuery(query);
586         }
587         mIsSearching = true;
588         rebindAdapters();
589         mHeader.setCollapsed(true);
590     }
591 
onClearSearchResult()592     public void onClearSearchResult() {
593         mIsSearching = false;
594         mHeader.setCollapsed(false);
595         rebindAdapters();
596         mHeader.reset(false);
597     }
598 
onSearchResultsChanged()599     public void onSearchResultsChanged() {
600         for (int i = 0; i < mAH.length; i++) {
601             if (mAH[i].recyclerView != null) {
602                 mAH[i].recyclerView.onSearchResultsChanged();
603             }
604         }
605     }
606 
setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled)607     public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) {
608         for (int i = 0; i < mAH.length; i++) {
609             mAH[i].applyVerticalFadingEdgeEnabled(enabled);
610         }
611     }
612 
addElevationController(RecyclerView.OnScrollListener scrollListener)613     public void addElevationController(RecyclerView.OnScrollListener scrollListener) {
614         if (!mUsingTabs) {
615             mAH[AdapterHolder.MAIN].recyclerView.addOnScrollListener(scrollListener);
616         }
617     }
618 
isHeaderVisible()619     public boolean isHeaderVisible() {
620         return mHeader != null && mHeader.getVisibility() == View.VISIBLE;
621     }
622 
623     /**
624      * Adds an update listener to {@param animator} that adds springs to the animation.
625      */
addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity , float progress )626     public void addSpringFromFlingUpdateListener(ValueAnimator animator,
627             float velocity /* release velocity */,
628             float progress /* portion of the distance to travel*/) {
629         animator.addListener(new AnimatorListenerAdapter() {
630             @Override
631             public void onAnimationStart(Animator animator) {
632                 float distance = (float) ((1 - progress) * getHeight()); // px
633                 float settleVelocity = Math.min(0, distance
634                         / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration())
635                         + velocity);
636                 absorbSwipeUpVelocity(Math.max(1000, Math.abs(
637                         Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER))));
638             }
639         });
640     }
641 
onPull(float deltaDistance, float displacement)642     public void onPull(float deltaDistance, float displacement) {
643         absorbPullDeltaDistance(PULL_MULTIPLIER * deltaDistance, PULL_MULTIPLIER * displacement);
644         // Current motion spec is to actually push and not pull
645         // on this surface. However, until EdgeEffect.onPush (b/190612804) is
646         // implemented at view level, we will simply pull
647     }
648 
649     @Override
getDrawingRect(Rect outRect)650     public void getDrawingRect(Rect outRect) {
651         super.getDrawingRect(outRect);
652         outRect.offset(0, (int) getTranslationY());
653     }
654 
655     @Override
setTranslationY(float translationY)656     public void setTranslationY(float translationY) {
657         super.setTranslationY(translationY);
658         invalidateHeader();
659     }
660 
setScrimView(ScrimView scrimView)661     public void setScrimView(ScrimView scrimView) {
662         mScrimView = scrimView;
663     }
664 
665     @Override
drawOnScrim(Canvas canvas)666     public void drawOnScrim(Canvas canvas) {
667         if (!mHeader.isHeaderProtectionSupported()) return;
668         mHeaderPaint.setColor(mHeaderColor);
669         mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor)));
670         if (mHeaderPaint.getColor() != mScrimColor && mHeaderPaint.getColor() != 0) {
671             int bottom = (int) (mSearchContainer.getBottom() + getTranslationY());
672             canvas.drawRect(0, 0, canvas.getWidth(), bottom, mHeaderPaint);
673             int tabsHeight = getFloatingHeaderView().getPeripheralProtectionHeight();
674             if (mTabsProtectionAlpha > 0 && tabsHeight != 0) {
675                 mHeaderPaint.setAlpha((int) (getAlpha() * mTabsProtectionAlpha));
676                 canvas.drawRect(0, bottom, canvas.getWidth(), bottom + tabsHeight, mHeaderPaint);
677             }
678         }
679     }
680 
681     public class AdapterHolder {
682         public static final int MAIN = 0;
683         public static final int WORK = 1;
684 
685         private final boolean mIsWork;
686         public final AllAppsGridAdapter adapter;
687         final LinearLayoutManager layoutManager;
688         final AlphabeticalAppsList appsList;
689         final Rect padding = new Rect();
690         AllAppsRecyclerView recyclerView;
691         boolean verticalFadingEdge;
692 
693 
AdapterHolder(boolean isWork)694         AdapterHolder(boolean isWork) {
695             mIsWork = isWork;
696             appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore,
697                     isWork ? mWorkManager.getAdapterProvider() : null);
698 
699             BaseAdapterProvider[] adapterProviders =
700                     isWork ? new BaseAdapterProvider[]{mSearchAdapterProvider,
701                             mWorkManager.getAdapterProvider()}
702                             : new BaseAdapterProvider[]{mSearchAdapterProvider};
703 
704             adapter = new AllAppsGridAdapter(mLauncher, getLayoutInflater(), appsList,
705                     adapterProviders);
706             appsList.setAdapter(adapter);
707             layoutManager = adapter.getLayoutManager();
708         }
709 
setup(@onNull View rv, @Nullable ItemInfoMatcher matcher)710         void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) {
711             appsList.updateItemFilter(matcher);
712             recyclerView = (AllAppsRecyclerView) rv;
713             recyclerView.setEdgeEffectFactory(createEdgeEffectFactory());
714             recyclerView.setApps(appsList);
715             recyclerView.setLayoutManager(layoutManager);
716             recyclerView.setAdapter(adapter);
717             recyclerView.setHasFixedSize(true);
718             // No animations will occur when changes occur to the items in this RecyclerView.
719             recyclerView.setItemAnimator(null);
720             recyclerView.addOnScrollListener(mScrollListener);
721             FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView);
722             recyclerView.addItemDecoration(focusedItemDecorator);
723             adapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
724             applyVerticalFadingEdgeEnabled(verticalFadingEdge);
725             applyPadding();
726             if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
727                 recyclerView.addItemDecoration(mSearchAdapterProvider.getDecorator());
728             }
729         }
730 
applyPadding()731         void applyPadding() {
732             if (recyclerView != null) {
733                 int bottomOffset = 0;
734                 if (mIsWork && mWorkManager.getWorkModeSwitch() != null) {
735                     bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
736                 }
737                 recyclerView.setPadding(padding.left, padding.top, padding.right,
738                         padding.bottom + bottomOffset);
739             }
740         }
741 
applyVerticalFadingEdgeEnabled(boolean enabled)742         public void applyVerticalFadingEdgeEnabled(boolean enabled) {
743             verticalFadingEdge = enabled;
744             mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
745                     && verticalFadingEdge);
746         }
747     }
748 
749 
updateHeaderScroll(int scrolledOffset)750     protected void updateHeaderScroll(int scrolledOffset) {
751 
752         float prog = Utilities.boundToRange((float) scrolledOffset / mHeaderThreshold, 0f, 1f);
753         int viewBG = ColorUtils.blendARGB(mScrimColor, mHeaderProtectionColor, prog);
754         int headerColor = ColorUtils.setAlphaComponent(viewBG,
755                 (int) (getSearchView().getAlpha() * 255));
756         int tabsAlpha = mHeader.getPeripheralProtectionHeight() == 0 ? 0
757                 : (int) (Utilities.boundToRange(
758                         (scrolledOffset + mHeader.mSnappedScrolledY) / mHeaderThreshold, 0f, 1f)
759                         * 255);
760         if (headerColor != mHeaderColor || mTabsProtectionAlpha != tabsAlpha) {
761             mHeaderColor = headerColor;
762             mTabsProtectionAlpha = tabsAlpha;
763             invalidateHeader();
764         }
765         if (mSearchUiManager.getEditText() != null) {
766             boolean bgVisible = mSearchUiManager.getBackgroundVisibility();
767             if (scrolledOffset == 0 && !mIsSearching) {
768                 bgVisible = true;
769             } else if (scrolledOffset > mHeaderThreshold) {
770                 bgVisible = false;
771             }
772             mSearchUiManager.setBackgroundVisibility(bgVisible, 1 - prog);
773         }
774     }
775 
776     /**
777      * redraws header protection
778      */
invalidateHeader()779     public void invalidateHeader() {
780         if (mScrimView != null && mHeader.isHeaderProtectionSupported()) {
781             mScrimView.invalidate();
782         }
783     }
784 }
785