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.Nullable;
34 import androidx.core.view.accessibility.AccessibilityEventCompat;
35 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
36 import androidx.core.view.accessibility.AccessibilityRecordCompat;
37 import androidx.recyclerview.widget.GridLayoutManager;
38 import androidx.recyclerview.widget.RecyclerView;
39 
40 import com.android.launcher3.BaseDraggingActivity;
41 import com.android.launcher3.BubbleTextView;
42 import com.android.launcher3.R;
43 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
44 import com.android.launcher3.model.AppLaunchTracker;
45 import com.android.launcher3.model.data.AppInfo;
46 import com.android.launcher3.util.PackageManagerHelper;
47 
48 import java.util.List;
49 
50 /**
51  * The grid view adapter of all the apps.
52  */
53 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
54 
55     public static final String TAG = "AppsGridAdapter";
56 
57     // A normal icon
58     public static final int VIEW_TYPE_ICON = 1 << 1;
59     // The message shown when there are no filtered results
60     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2;
61     // The message to continue to a market search when there are no filtered results
62     public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 3;
63 
64     // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
65     // but differ in enough attributes to require different view types
66 
67     // A divider that separates the apps list and the search market button
68     public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4;
69 
70     // Common view type masks
71     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
72     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
73 
74     /**
75      * ViewHolder for each icon.
76      */
77     public static class ViewHolder extends RecyclerView.ViewHolder {
78 
ViewHolder(View v)79         public ViewHolder(View v) {
80             super(v);
81         }
82     }
83 
84     /**
85      * A subclass of GridLayoutManager that overrides accessibility values during app search.
86      */
87     public class AppsGridLayoutManager extends GridLayoutManager {
88 
AppsGridLayoutManager(Context context)89         public AppsGridLayoutManager(Context context) {
90             super(context, 1, GridLayoutManager.VERTICAL, false);
91         }
92 
93         @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)94         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
95             super.onInitializeAccessibilityEvent(event);
96 
97             // Ensure that we only report the number apps for accessibility not including other
98             // adapter views
99             final AccessibilityRecordCompat record = AccessibilityEventCompat
100                     .asRecord(event);
101             record.setItemCount(mApps.getNumFilteredApps());
102             record.setFromIndex(Math.max(0,
103                     record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
104             record.setToIndex(Math.max(0,
105                     record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
106         }
107 
108         @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)109         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
110                 RecyclerView.State state) {
111             return super.getRowCountForAccessibility(recycler, state) -
112                     getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
113         }
114 
115         @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)116         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
117                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
118             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
119 
120             ViewGroup.LayoutParams lp = host.getLayoutParams();
121             AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
122             if (!(lp instanceof LayoutParams) || (cic == null)) {
123                 return;
124             }
125             LayoutParams glp = (LayoutParams) lp;
126             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
127                     cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
128                     cic.getRowSpan(),
129                     cic.getColumnIndex(),
130                     cic.getColumnSpan(),
131                     cic.isHeading(),
132                     cic.isSelected()));
133         }
134 
135         /**
136          * Returns the number of rows before {@param adapterPosition}, including this position
137          * which should not be counted towards the collection info.
138          */
getRowsNotForAccessibility(int adapterPosition)139         private int getRowsNotForAccessibility(int adapterPosition) {
140             List<AdapterItem> items = mApps.getAdapterItems();
141             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
142             int extraRows = 0;
143             for (int i = 0; i <= adapterPosition; i++) {
144                 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_ICON)) {
145                     extraRows++;
146                 }
147             }
148             return extraRows;
149         }
150     }
151 
152     /**
153      * Helper class to size the grid items.
154      */
155     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
156 
GridSpanSizer()157         public GridSpanSizer() {
158             super();
159             setSpanIndexCacheEnabled(true);
160         }
161 
162         @Override
getSpanSize(int position)163         public int getSpanSize(int position) {
164             if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
165                 return 1;
166             } else {
167                 // Section breaks span the full width
168                 return mAppsPerRow;
169             }
170         }
171     }
172 
173     private final BaseDraggingActivity mLauncher;
174     private final LayoutInflater mLayoutInflater;
175     private final AlphabeticalAppsList mApps;
176     private final GridLayoutManager mGridLayoutMgr;
177     private final GridSpanSizer mGridSizer;
178 
179     private final OnClickListener mOnIconClickListener;
180     private OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS;
181 
182     private int mAppsPerRow;
183 
184     private OnFocusChangeListener mIconFocusListener;
185 
186     // The text to show when there are no search results and no market search handler.
187     protected String mEmptySearchMessage;
188     // The intent to send off to the market app, updated each time the search query changes.
189     private Intent mMarketSearchIntent;
190 
AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, AlphabeticalAppsList apps)191     public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater,
192             AlphabeticalAppsList apps) {
193         Resources res = launcher.getResources();
194         mLauncher = launcher;
195         mApps = apps;
196         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
197         mGridSizer = new GridSpanSizer();
198         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
199         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
200         mLayoutInflater = inflater;
201 
202         mOnIconClickListener = launcher.getItemOnClickListener();
203 
204         setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
205     }
206 
setAppsPerRow(int appsPerRow)207     public void setAppsPerRow(int appsPerRow) {
208         mAppsPerRow = appsPerRow;
209         mGridLayoutMgr.setSpanCount(mAppsPerRow);
210     }
211 
212     /**
213      * Sets the long click listener for icons
214      */
setOnIconLongClickListener(@ullable OnLongClickListener listener)215     public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) {
216         mOnIconLongClickListener = listener;
217     }
218 
isDividerViewType(int viewType)219     public static boolean isDividerViewType(int viewType) {
220         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
221     }
222 
isIconViewType(int viewType)223     public static boolean isIconViewType(int viewType) {
224         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
225     }
226 
isViewType(int viewType, int viewTypeMask)227     public static boolean isViewType(int viewType, int viewTypeMask) {
228         return (viewType & viewTypeMask) != 0;
229     }
230 
setIconFocusListener(OnFocusChangeListener focusListener)231     public void setIconFocusListener(OnFocusChangeListener focusListener) {
232         mIconFocusListener = focusListener;
233     }
234 
235     /**
236      * Sets the last search query that was made, used to show when there are no results and to also
237      * seed the intent for searching the market.
238      */
setLastSearchQuery(String query)239     public void setLastSearchQuery(String query) {
240         Resources res = mLauncher.getResources();
241         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
242         mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
243     }
244 
245     /**
246      * Returns the grid layout manager.
247      */
getLayoutManager()248     public GridLayoutManager getLayoutManager() {
249         return mGridLayoutMgr;
250     }
251 
252     @Override
onCreateViewHolder(ViewGroup parent, int viewType)253     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
254         switch (viewType) {
255             case VIEW_TYPE_ICON:
256                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
257                         R.layout.all_apps_icon, parent, false);
258                 icon.setOnClickListener(mOnIconClickListener);
259                 icon.setOnLongClickListener(mOnIconLongClickListener);
260                 icon.setLongPressTimeoutFactor(1f);
261                 icon.setOnFocusChangeListener(mIconFocusListener);
262 
263                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
264                 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
265                 return new ViewHolder(icon);
266             case VIEW_TYPE_EMPTY_SEARCH:
267                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
268                         parent, false));
269             case VIEW_TYPE_SEARCH_MARKET:
270                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
271                         parent, false);
272                 searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely(
273                         v, mMarketSearchIntent, null, AppLaunchTracker.CONTAINER_SEARCH));
274                 return new ViewHolder(searchMarketView);
275             case VIEW_TYPE_ALL_APPS_DIVIDER:
276                 return new ViewHolder(mLayoutInflater.inflate(
277                         R.layout.all_apps_divider, parent, false));
278             default:
279                 throw new RuntimeException("Unexpected view type");
280         }
281     }
282 
283     @Override
onBindViewHolder(ViewHolder holder, int position)284     public void onBindViewHolder(ViewHolder holder, int position) {
285         switch (holder.getItemViewType()) {
286             case VIEW_TYPE_ICON:
287                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
288                 BubbleTextView icon = (BubbleTextView) holder.itemView;
289                 icon.reset();
290                 icon.applyFromApplicationInfo(info);
291                 break;
292             case VIEW_TYPE_EMPTY_SEARCH:
293                 TextView emptyViewText = (TextView) holder.itemView;
294                 emptyViewText.setText(mEmptySearchMessage);
295                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
296                         Gravity.START | Gravity.CENTER_VERTICAL);
297                 break;
298             case VIEW_TYPE_SEARCH_MARKET:
299                 TextView searchView = (TextView) holder.itemView;
300                 if (mMarketSearchIntent != null) {
301                     searchView.setVisibility(View.VISIBLE);
302                 } else {
303                     searchView.setVisibility(View.GONE);
304                 }
305                 break;
306             case VIEW_TYPE_ALL_APPS_DIVIDER:
307                 // nothing to do
308                 break;
309         }
310     }
311 
312     @Override
onFailedToRecycleView(ViewHolder holder)313     public boolean onFailedToRecycleView(ViewHolder holder) {
314         // Always recycle and we will reset the view when it is bound
315         return true;
316     }
317 
318     @Override
getItemCount()319     public int getItemCount() {
320         return mApps.getAdapterItems().size();
321     }
322 
323     @Override
getItemViewType(int position)324     public int getItemViewType(int position) {
325         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
326         return item.viewType;
327     }
328 
329 }
330