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