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.touch.ItemLongClickListener.INSTANCE_ALL_APPS;
19 
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.res.Resources;
23 import android.view.Gravity;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.View.OnClickListener;
27 import android.view.View.OnFocusChangeListener;
28 import android.view.View.OnLongClickListener;
29 import android.view.ViewGroup;
30 import android.view.accessibility.AccessibilityEvent;
31 import android.widget.TextView;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.core.view.accessibility.AccessibilityEventCompat;
36 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
37 import androidx.core.view.accessibility.AccessibilityRecordCompat;
38 import androidx.recyclerview.widget.GridLayoutManager;
39 import androidx.recyclerview.widget.RecyclerView;
40 
41 import com.android.launcher3.BaseDraggingActivity;
42 import com.android.launcher3.BubbleTextView;
43 import com.android.launcher3.R;
44 import com.android.launcher3.config.FeatureFlags;
45 import com.android.launcher3.model.data.AppInfo;
46 import com.android.launcher3.model.data.ItemInfoWithIcon;
47 import com.android.launcher3.util.PackageManagerHelper;
48 
49 import java.util.Arrays;
50 import java.util.List;
51 
52 /**
53  * The grid view adapter of all the apps.
54  */
55 public class AllAppsGridAdapter extends
56         RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
57 
58     public static final String TAG = "AppsGridAdapter";
59 
60     // A normal icon
61     public static final int VIEW_TYPE_ICON = 1 << 1;
62     // The message shown when there are no filtered results
63     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2;
64     // The message to continue to a market search when there are no filtered results
65     public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 3;
66 
67     // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
68     // but differ in enough attributes to require different view types
69 
70     // A divider that separates the apps list and the search market button
71     public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4;
72 
73     // Common view type masks
74     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
75     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
76 
77 
78     private final BaseAdapterProvider[] mAdapterProviders;
79 
80     /**
81      * ViewHolder for each icon.
82      */
83     public static class ViewHolder extends RecyclerView.ViewHolder {
84 
ViewHolder(View v)85         public ViewHolder(View v) {
86             super(v);
87         }
88     }
89 
90     /**
91      * Info about a particular adapter item (can be either section or app)
92      */
93     public static class AdapterItem {
94         /** Common properties */
95         // The index of this adapter item in the list
96         public int position;
97         // The type of this item
98         public int viewType;
99 
100         // The section name of this item.  Note that there can be multiple items with different
101         // sectionNames in the same section
102         public String sectionName = null;
103         // The row that this item shows up on
104         public int rowIndex;
105         // The index of this app in the row
106         public int rowAppIndex;
107         // The associated ItemInfoWithIcon for the item
108         public ItemInfoWithIcon itemInfo = null;
109         // The index of this app not including sections
110         public int appIndex = -1;
111         // Search section associated to result
112         public DecorationInfo decorationInfo = null;
113 
114         /**
115          * Factory method for AppIcon AdapterItem
116          */
asApp(int pos, String sectionName, AppInfo appInfo, int appIndex)117         public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
118                 int appIndex) {
119             AdapterItem item = new AdapterItem();
120             item.viewType = VIEW_TYPE_ICON;
121             item.position = pos;
122             item.sectionName = sectionName;
123             item.itemInfo = appInfo;
124             item.appIndex = appIndex;
125             return item;
126         }
127 
128         /**
129          * Factory method for empty search results view
130          */
asEmptySearch(int pos)131         public static AdapterItem asEmptySearch(int pos) {
132             AdapterItem item = new AdapterItem();
133             item.viewType = VIEW_TYPE_EMPTY_SEARCH;
134             item.position = pos;
135             return item;
136         }
137 
138         /**
139          * Factory method for a dividerView in AllAppsSearch
140          */
asAllAppsDivider(int pos)141         public static AdapterItem asAllAppsDivider(int pos) {
142             AdapterItem item = new AdapterItem();
143             item.viewType = VIEW_TYPE_ALL_APPS_DIVIDER;
144             item.position = pos;
145             return item;
146         }
147 
148         /**
149          * Factory method for a market search button
150          */
asMarketSearch(int pos)151         public static AdapterItem asMarketSearch(int pos) {
152             AdapterItem item = new AdapterItem();
153             item.viewType = VIEW_TYPE_SEARCH_MARKET;
154             item.position = pos;
155             return item;
156         }
157 
isCountedForAccessibility()158         protected boolean isCountedForAccessibility() {
159             return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET;
160         }
161     }
162 
163     /**
164      * A subclass of GridLayoutManager that overrides accessibility values during app search.
165      */
166     public class AppsGridLayoutManager extends GridLayoutManager {
167 
AppsGridLayoutManager(Context context)168         public AppsGridLayoutManager(Context context) {
169             super(context, 1, GridLayoutManager.VERTICAL, false);
170         }
171 
172         @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)173         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
174             super.onInitializeAccessibilityEvent(event);
175 
176             // Ensure that we only report the number apps for accessibility not including other
177             // adapter views
178             final AccessibilityRecordCompat record = AccessibilityEventCompat
179                     .asRecord(event);
180             record.setItemCount(mApps.getNumFilteredApps());
181             record.setFromIndex(Math.max(0,
182                     record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
183             record.setToIndex(Math.max(0,
184                     record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
185         }
186 
187         @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)188         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
189                 RecyclerView.State state) {
190             return super.getRowCountForAccessibility(recycler, state) -
191                     getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
192         }
193 
194         @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)195         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
196                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
197             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
198 
199             ViewGroup.LayoutParams lp = host.getLayoutParams();
200             AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
201             if (!(lp instanceof LayoutParams) || (cic == null)) {
202                 return;
203             }
204             LayoutParams glp = (LayoutParams) lp;
205             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
206                     cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
207                     cic.getRowSpan(),
208                     cic.getColumnIndex(),
209                     cic.getColumnSpan(),
210                     cic.isHeading(),
211                     cic.isSelected()));
212         }
213 
214         /**
215          * Returns the number of rows before {@param adapterPosition}, including this position
216          * which should not be counted towards the collection info.
217          */
getRowsNotForAccessibility(int adapterPosition)218         private int getRowsNotForAccessibility(int adapterPosition) {
219             List<AdapterItem> items = mApps.getAdapterItems();
220             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
221             int extraRows = 0;
222             for (int i = 0; i <= adapterPosition; i++) {
223                 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_ICON)) {
224                     extraRows++;
225                 }
226             }
227             return extraRows;
228         }
229     }
230 
231     /**
232      * Helper class to size the grid items.
233      */
234     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
235 
GridSpanSizer()236         public GridSpanSizer() {
237             super();
238             setSpanIndexCacheEnabled(true);
239         }
240 
241         @Override
getSpanSize(int position)242         public int getSpanSize(int position) {
243             int viewType = mApps.getAdapterItems().get(position).viewType;
244             int totalSpans = mGridLayoutMgr.getSpanCount();
245             if (isIconViewType(viewType)) {
246                 return totalSpans / mAppsPerRow;
247             } else {
248                 BaseAdapterProvider adapterProvider = getAdapterProvider(viewType);
249                 if (adapterProvider != null) {
250                     return totalSpans / adapterProvider.getItemsPerRow(viewType, mAppsPerRow);
251                 }
252 
253                 // Section breaks span the full width
254                 return totalSpans;
255             }
256         }
257     }
258 
259     private final BaseDraggingActivity mLauncher;
260     private final LayoutInflater mLayoutInflater;
261     private final AlphabeticalAppsList mApps;
262     private final GridLayoutManager mGridLayoutMgr;
263     private final GridSpanSizer mGridSizer;
264 
265     private final OnClickListener mOnIconClickListener;
266     private OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS;
267 
268     private int mAppsPerRow;
269 
270     private OnFocusChangeListener mIconFocusListener;
271 
272     // The text to show when there are no search results and no market search handler.
273     protected String mEmptySearchMessage;
274     // The intent to send off to the market app, updated each time the search query changes.
275     private Intent mMarketSearchIntent;
276 
277     private final int mExtraHeight;
278 
AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders)279     public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater,
280             AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders) {
281         Resources res = launcher.getResources();
282         mLauncher = launcher;
283         mApps = apps;
284         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
285         mGridSizer = new GridSpanSizer();
286         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
287         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
288         mLayoutInflater = inflater;
289 
290         mOnIconClickListener = launcher.getItemOnClickListener();
291 
292         mAdapterProviders = adapterProviders;
293         setAppsPerRow(mLauncher.getDeviceProfile().numShownAllAppsColumns);
294         mExtraHeight = launcher.getResources().getDimensionPixelSize(R.dimen.all_apps_height_extra);
295     }
296 
setAppsPerRow(int appsPerRow)297     public void setAppsPerRow(int appsPerRow) {
298         mAppsPerRow = appsPerRow;
299         int totalSpans = mAppsPerRow;
300         for (BaseAdapterProvider adapterProvider : mAdapterProviders) {
301             for (int itemPerRow : adapterProvider.getSupportedItemsPerRowArray()) {
302                 if (totalSpans % itemPerRow != 0) {
303                     totalSpans *= itemPerRow;
304                 }
305             }
306         }
307         mGridLayoutMgr.setSpanCount(totalSpans);
308     }
309 
310     /**
311      * Sets the long click listener for icons
312      */
setOnIconLongClickListener(@ullable OnLongClickListener listener)313     public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) {
314         mOnIconLongClickListener = listener;
315     }
316 
isDividerViewType(int viewType)317     public static boolean isDividerViewType(int viewType) {
318         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
319     }
320 
isIconViewType(int viewType)321     public static boolean isIconViewType(int viewType) {
322         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
323     }
324 
isViewType(int viewType, int viewTypeMask)325     public static boolean isViewType(int viewType, int viewTypeMask) {
326         return (viewType & viewTypeMask) != 0;
327     }
328 
setIconFocusListener(OnFocusChangeListener focusListener)329     public void setIconFocusListener(OnFocusChangeListener focusListener) {
330         mIconFocusListener = focusListener;
331     }
332 
333     /**
334      * Sets the last search query that was made, used to show when there are no results and to also
335      * seed the intent for searching the market.
336      */
setLastSearchQuery(String query)337     public void setLastSearchQuery(String query) {
338         Resources res = mLauncher.getResources();
339         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
340         mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
341     }
342 
343     /**
344      * Returns the grid layout manager.
345      */
getLayoutManager()346     public GridLayoutManager getLayoutManager() {
347         return mGridLayoutMgr;
348     }
349 
350     @Override
onCreateViewHolder(ViewGroup parent, int viewType)351     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
352         switch (viewType) {
353             case VIEW_TYPE_ICON:
354                 int layout = !FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() ? R.layout.all_apps_icon
355                         : R.layout.all_apps_icon_twoline;
356                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
357                         layout, parent, false);
358                 icon.setLongPressTimeoutFactor(1f);
359                 icon.setOnFocusChangeListener(mIconFocusListener);
360                 icon.setOnClickListener(mOnIconClickListener);
361                 icon.setOnLongClickListener(mOnIconLongClickListener);
362                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
363                 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
364                 if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) {
365                     icon.getLayoutParams().height += mExtraHeight;
366                 }
367                 return new ViewHolder(icon);
368             case VIEW_TYPE_EMPTY_SEARCH:
369                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
370                         parent, false));
371             case VIEW_TYPE_SEARCH_MARKET:
372                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
373                         parent, false);
374                 searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely(
375                         v, mMarketSearchIntent, null));
376                 return new ViewHolder(searchMarketView);
377             case VIEW_TYPE_ALL_APPS_DIVIDER:
378                 return new ViewHolder(mLayoutInflater.inflate(
379                         R.layout.all_apps_divider, parent, false));
380             default:
381                 BaseAdapterProvider adapterProvider = getAdapterProvider(viewType);
382                 if (adapterProvider != null) {
383                     return adapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType);
384                 }
385                 throw new RuntimeException("Unexpected view type" + viewType);
386         }
387     }
388 
389     @Override
onBindViewHolder(ViewHolder holder, int position)390     public void onBindViewHolder(ViewHolder holder, int position) {
391         switch (holder.getItemViewType()) {
392             case VIEW_TYPE_ICON:
393                 AdapterItem adapterItem = mApps.getAdapterItems().get(position);
394                 BubbleTextView icon = (BubbleTextView) holder.itemView;
395                 icon.reset();
396                 if (adapterItem.itemInfo instanceof AppInfo) {
397                     icon.applyFromApplicationInfo((AppInfo) adapterItem.itemInfo);
398                 } else {
399                     icon.applyFromItemInfoWithIcon(adapterItem.itemInfo);
400                 }
401                 break;
402             case VIEW_TYPE_EMPTY_SEARCH:
403                 TextView emptyViewText = (TextView) holder.itemView;
404                 emptyViewText.setText(mEmptySearchMessage);
405                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
406                         Gravity.START | Gravity.CENTER_VERTICAL);
407                 break;
408             case VIEW_TYPE_SEARCH_MARKET:
409                 TextView searchView = (TextView) holder.itemView;
410                 if (mMarketSearchIntent != null) {
411                     searchView.setVisibility(View.VISIBLE);
412                 } else {
413                     searchView.setVisibility(View.GONE);
414                 }
415                 break;
416             case VIEW_TYPE_ALL_APPS_DIVIDER:
417                 // nothing to do
418                 break;
419             default:
420                 BaseAdapterProvider adapterProvider = getAdapterProvider(holder.getItemViewType());
421                 if (adapterProvider != null) {
422                     adapterProvider.onBindView(holder, position);
423                 }
424         }
425     }
426 
427     @Override
onViewRecycled(@onNull ViewHolder holder)428     public void onViewRecycled(@NonNull ViewHolder holder) {
429         super.onViewRecycled(holder);
430     }
431 
432     @Override
onFailedToRecycleView(ViewHolder holder)433     public boolean onFailedToRecycleView(ViewHolder holder) {
434         // Always recycle and we will reset the view when it is bound
435         return true;
436     }
437 
438     @Override
getItemCount()439     public int getItemCount() {
440         return mApps.getAdapterItems().size();
441     }
442 
443     @Override
getItemViewType(int position)444     public int getItemViewType(int position) {
445         AdapterItem item = mApps.getAdapterItems().get(position);
446         return item.viewType;
447     }
448 
449     @Nullable
getAdapterProvider(int viewType)450     private BaseAdapterProvider getAdapterProvider(int viewType) {
451         return Arrays.stream(mAdapterProviders).filter(
452                 adapterProvider -> adapterProvider.isViewSupported(viewType)).findFirst().orElse(
453                 null);
454     }
455 }
456