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.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
19 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
20 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
21 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION;
25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN;
26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP;
27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN;
28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE;
30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND;
31 import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT;
32 import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT;
33 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING;
34 
35 import android.content.Context;
36 import android.graphics.Canvas;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.view.View;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.core.util.Consumer;
44 import androidx.recyclerview.widget.RecyclerView;
45 
46 import com.android.launcher3.DeviceProfile;
47 import com.android.launcher3.ExtendedEditText;
48 import com.android.launcher3.FastScrollRecyclerView;
49 import com.android.launcher3.LauncherAppState;
50 import com.android.launcher3.R;
51 import com.android.launcher3.Utilities;
52 import com.android.launcher3.logging.StatsLogManager;
53 import com.android.launcher3.views.ActivityContext;
54 
55 import java.util.List;
56 
57 /**
58  * A RecyclerView with custom fast scroll support for the all apps view.
59  */
60 public class AllAppsRecyclerView extends FastScrollRecyclerView {
61     protected static final String TAG = "AllAppsRecyclerView";
62     private static final boolean DEBUG = false;
63     private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING);
64     private Consumer<View> mChildAttachedConsumer;
65 
66     protected final int mNumAppsPerRow;
67     private final AllAppsFastScrollHelper mFastScrollHelper;
68     private int mCumulativeVerticalScroll;
69 
70     protected AlphabeticalAppsList<?> mApps;
71 
AllAppsRecyclerView(Context context)72     public AllAppsRecyclerView(Context context) {
73         this(context, null);
74     }
75 
AllAppsRecyclerView(Context context, AttributeSet attrs)76     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
77         this(context, attrs, 0);
78     }
79 
AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)80     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
81         this(context, attrs, defStyleAttr, 0);
82     }
83 
AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)84     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
85             int defStyleRes) {
86         super(context, attrs, defStyleAttr);
87         mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns;
88         mFastScrollHelper = new AllAppsFastScrollHelper(this);
89     }
90 
91     /**
92      * Sets the list of apps in this view, used to determine the fastscroll position.
93      */
setApps(AlphabeticalAppsList<?> apps)94     public void setApps(AlphabeticalAppsList<?> apps) {
95         mApps = apps;
96     }
97 
getApps()98     public AlphabeticalAppsList<?> getApps() {
99         return mApps;
100     }
101 
updatePoolSize()102     protected void updatePoolSize() {
103         updatePoolSize(false);
104     }
105 
updatePoolSize(boolean hasWorkProfile)106     void updatePoolSize(boolean hasWorkProfile) {
107         DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile();
108         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
109         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
110         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
111 
112         // By default the max num of pool size for app icons is num of app icons in one page of
113         // all apps.
114         int maxPoolSizeForAppIcons = grid.getMaxAllAppsRowCount()
115                 * grid.numShownAllAppsColumns;
116         if (ALL_APPS_GONE_VISIBILITY.get() && ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
117             // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to
118             // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows +
119             // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly.
120             maxPoolSizeForAppIcons +=
121                     PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
122         }
123         if (hasWorkProfile) {
124             maxPoolSizeForAppIcons *= 2;
125         }
126         pool.setMaxRecycledViews(
127                 AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons);
128     }
129 
130     @Override
onDraw(Canvas c)131     public void onDraw(Canvas c) {
132         if (DEBUG) {
133             Log.d(TAG, "onDraw at = " + System.currentTimeMillis());
134         }
135         if (DEBUG_LATENCY) {
136             Log.d(SEARCH_LOGGING,  getClass().getSimpleName() + " onDraw; time stamp = "
137                     + System.currentTimeMillis());
138         }
139         super.onDraw(c);
140     }
141 
142     @Override
onSizeChanged(int w, int h, int oldw, int oldh)143     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
144         updatePoolSize();
145     }
146 
onSearchResultsChanged()147     public void onSearchResultsChanged() {
148         // Always scroll the view to the top so the user can see the changed results
149         scrollToTop();
150     }
151 
152     @Override
onScrollStateChanged(int state)153     public void onScrollStateChanged(int state) {
154         super.onScrollStateChanged(state);
155 
156         StatsLogManager mgr = ActivityContext.lookupContext(getContext()).getStatsLogManager();
157         switch (state) {
158             case SCROLL_STATE_DRAGGING:
159                 mCumulativeVerticalScroll = 0;
160                 requestFocus();
161                 mgr.logger().sendToInteractionJankMonitor(
162                         LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
163                 ActivityContext.lookupContext(getContext()).hideKeyboard();
164                 break;
165             case SCROLL_STATE_IDLE:
166                 mgr.logger().sendToInteractionJankMonitor(
167                         LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END, this);
168                 logCumulativeVerticalScroll();
169                 break;
170         }
171     }
172 
173     @Override
onScrolled(int dx, int dy)174     public void onScrolled(int dx, int dy) {
175         super.onScrolled(dx, dy);
176         mCumulativeVerticalScroll += dy;
177     }
178 
179     /**
180      * Maps the touch (from 0..1) to the adapter position that should be visible.
181      */
182     @Override
scrollToPositionAtProgress(float touchFraction)183     public CharSequence scrollToPositionAtProgress(float touchFraction) {
184         int rowCount = mApps.getNumAppRows();
185         if (rowCount == 0) {
186             return "";
187         }
188 
189         // Find the fastscroll section that maps to this touch fraction
190         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
191                 mApps.getFastScrollerSections();
192         int count = fastScrollSections.size();
193         if (count == 0) {
194             return "";
195         }
196         int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1);
197         AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index);
198         mFastScrollHelper.smoothScrollToSection(section);
199         return section.sectionName;
200     }
201 
202     @Override
onFastScrollCompleted()203     public void onFastScrollCompleted() {
204         super.onFastScrollCompleted();
205         mFastScrollHelper.onFastScrollCompleted();
206     }
207 
208     @Override
isPaddingOffsetRequired()209     protected boolean isPaddingOffsetRequired() {
210         return true;
211     }
212 
213     @Override
getTopPaddingOffset()214     protected int getTopPaddingOffset() {
215         return -getPaddingTop();
216     }
217 
218     /**
219      * Updates the bounds for the scrollbar.
220      */
221     @Override
onUpdateScrollbar(int dy)222     public void onUpdateScrollbar(int dy) {
223         if (mApps == null) {
224             return;
225         }
226         List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems();
227 
228         // Skip early if there are no items or we haven't been measured
229         if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
230             mScrollbar.setThumbOffsetY(-1);
231             return;
232         }
233 
234         // Skip early if, there no child laid out in the container.
235         int scrollY = computeVerticalScrollOffset();
236         if (scrollY < 0) {
237             mScrollbar.setThumbOffsetY(-1);
238             return;
239         }
240 
241         // Only show the scrollbar if there is height to be scrolled
242         int availableScrollBarHeight = getAvailableScrollBarHeight();
243         int availableScrollHeight = getAvailableScrollHeight();
244         if (availableScrollHeight <= 0) {
245             mScrollbar.setThumbOffsetY(-1);
246             return;
247         }
248 
249         if (mScrollbar.isThumbDetached()) {
250             if (!mScrollbar.isDraggingThumb()) {
251                 // Calculate the current scroll position, the scrollY of the recycler view accounts
252                 // for the view padding, while the scrollBarY is drawn right up to the background
253                 // padding (ignoring padding)
254                 int scrollBarY = (int)
255                         (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
256 
257                 int thumbScrollY = mScrollbar.getThumbOffsetY();
258                 int diffScrollY = scrollBarY - thumbScrollY;
259                 if (diffScrollY * dy > 0f) {
260                     // User is scrolling in the same direction the thumb needs to catch up to the
261                     // current scroll position.  We do this by mapping the difference in movement
262                     // from the original scroll bar position to the difference in movement necessary
263                     // in the detached thumb position to ensure that both speed towards the same
264                     // position at either end of the list.
265                     if (dy < 0) {
266                         int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY);
267                         thumbScrollY += Math.max(offset, diffScrollY);
268                     } else {
269                         int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) /
270                                 (float) (availableScrollBarHeight - scrollBarY));
271                         thumbScrollY += Math.min(offset, diffScrollY);
272                     }
273                     thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
274                     mScrollbar.setThumbOffsetY(thumbScrollY);
275                     if (scrollBarY == thumbScrollY) {
276                         mScrollbar.reattachThumbToScroll();
277                     }
278                 } else {
279                     // User is scrolling in an opposite direction to the direction that the thumb
280                     // needs to catch up to the scroll position.  Do nothing except for updating
281                     // the scroll bar x to match the thumb width.
282                     mScrollbar.setThumbOffsetY(thumbScrollY);
283                 }
284             }
285         } else {
286             synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight);
287         }
288     }
289 
290     /**
291      * This will be called just before a new child is attached to the window. Passing in null will
292      * remove the consumer.
293      */
setChildAttachedConsumer(@ullable Consumer<View> childAttachedConsumer)294     protected void setChildAttachedConsumer(@Nullable Consumer<View> childAttachedConsumer) {
295         mChildAttachedConsumer = childAttachedConsumer;
296     }
297 
298     @Override
onChildAttachedToWindow(@onNull View child)299     public void onChildAttachedToWindow(@NonNull View child) {
300         if (mChildAttachedConsumer != null) {
301             mChildAttachedConsumer.accept(child);
302         }
303         super.onChildAttachedToWindow(child);
304     }
305 
306     @Override
getScrollBarTop()307     public int getScrollBarTop() {
308         return getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding);
309     }
310 
311     @Override
getScrollBarMarginBottom()312     public int getScrollBarMarginBottom() {
313         return getRootWindowInsets() == null ? 0
314                 : getRootWindowInsets().getSystemWindowInsetBottom();
315     }
316 
317     @Override
hasOverlappingRendering()318     public boolean hasOverlappingRendering() {
319         return false;
320     }
321 
logCumulativeVerticalScroll()322     private void logCumulativeVerticalScroll() {
323         ActivityContext context = ActivityContext.lookupContext(getContext());
324         StatsLogManager mgr = context.getStatsLogManager();
325         ActivityAllAppsContainerView<?> appsView = context.getAppsView();
326         ExtendedEditText editText = appsView.getSearchUiManager().getEditText();
327         ContainerInfo containerInfo = ContainerInfo.newBuilder().setSearchResultContainer(
328                 SearchResultContainer
329                         .newBuilder()
330                         .setQueryLength((editText == null) ? -1 : editText.length())).build();
331         if (mCumulativeVerticalScroll == 0) {
332             // mCumulativeVerticalScroll == 0 when user comes back to original position, we
333             // don't know the direction of scrolling.
334             mgr.logger().withContainerInfo(containerInfo).log(
335                     LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION);
336             return;
337         } else if (appsView.isSearching()) {
338             // In search results page
339             mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0)
340                     ? LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN
341                     : LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP);
342             return;
343         } else if (appsView.mViewPager != null) {
344             int currentPage = appsView.mViewPager.getCurrentPage();
345             if (currentPage == ActivityAllAppsContainerView.AdapterHolder.WORK) {
346                 // In work A-Z list
347                 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0)
348                         ? LAUNCHER_WORK_FAB_BUTTON_COLLAPSE
349                         : LAUNCHER_WORK_FAB_BUTTON_EXTEND);
350                 return;
351             }
352         }
353         // In personal A-Z list
354         mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0)
355                 ? LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN
356                 : LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP);
357     }
358 }
359