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 android.content.Context;
19 import android.content.Intent;
20 import android.content.res.Resources;
21 import android.graphics.Point;
22 import android.support.v4.view.accessibility.AccessibilityEventCompat;
23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
24 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
25 import android.support.v7.widget.GridLayoutManager;
26 import android.support.v7.widget.RecyclerView;
27 import android.view.Gravity;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.View.OnFocusChangeListener;
31 import android.view.ViewConfiguration;
32 import android.view.ViewGroup;
33 import android.view.accessibility.AccessibilityEvent;
34 import android.widget.TextView;
35 
36 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
37 import com.android.launcher3.AppInfo;
38 import com.android.launcher3.BubbleTextView;
39 import com.android.launcher3.Launcher;
40 import com.android.launcher3.R;
41 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
42 import com.android.launcher3.discovery.AppDiscoveryItemView;
43 
44 import java.util.List;
45 
46 /**
47  * The grid view adapter of all the apps.
48  */
49 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
50 
51     public static final String TAG = "AppsGridAdapter";
52 
53     // A normal icon
54     public static final int VIEW_TYPE_ICON = 1 << 1;
55     // A prediction icon
56     public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2;
57     // The message shown when there are no filtered results
58     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3;
59     // The message to continue to a market search when there are no filtered results
60     public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4;
61 
62     // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
63     // but differ in enough attributes to require different view types
64 
65     // A divider that separates the apps list and the search market button
66     public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5;
67     // The divider under the search field
68     public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6;
69     // The divider that separates prediction icons from the app list
70     public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7;
71     public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 8;
72     public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 9;
73 
74     // Common view type masks
75     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER
76             | VIEW_TYPE_SEARCH_MARKET_DIVIDER
77             | VIEW_TYPE_PREDICTION_DIVIDER;
78     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
79             | VIEW_TYPE_PREDICTION_ICON;
80     public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
81             | VIEW_TYPE_DISCOVERY_ITEM;
82 
83 
84     public interface BindViewCallback {
onBindView(ViewHolder holder)85         void onBindView(ViewHolder holder);
86     }
87 
88     /**
89      * ViewHolder for each icon.
90      */
91     public static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View v)92         public ViewHolder(View v) {
93             super(v);
94         }
95     }
96 
97     /**
98      * A subclass of GridLayoutManager that overrides accessibility values during app search.
99      */
100     public class AppsGridLayoutManager extends GridLayoutManager {
101 
AppsGridLayoutManager(Context context)102         public AppsGridLayoutManager(Context context) {
103             super(context, 1, GridLayoutManager.VERTICAL, false);
104         }
105 
106         @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)107         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
108             super.onInitializeAccessibilityEvent(event);
109 
110             // Ensure that we only report the number apps for accessibility not including other
111             // adapter views
112             final AccessibilityRecordCompat record = AccessibilityEventCompat
113                     .asRecord(event);
114             record.setItemCount(mApps.getNumFilteredApps());
115             record.setFromIndex(Math.max(0,
116                     record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
117             record.setToIndex(Math.max(0,
118                     record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
119         }
120 
121         @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)122         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
123                 RecyclerView.State state) {
124             return super.getRowCountForAccessibility(recycler, state) -
125                     getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
126         }
127 
128         @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)129         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
130                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
131             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
132 
133             ViewGroup.LayoutParams lp = host.getLayoutParams();
134             AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
135             if (!(lp instanceof LayoutParams) || (cic == null)) {
136                 return;
137             }
138             LayoutParams glp = (LayoutParams) lp;
139             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
140                     cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
141                     cic.getRowSpan(),
142                     cic.getColumnIndex(),
143                     cic.getColumnSpan(),
144                     cic.isHeading(),
145                     cic.isSelected()));
146         }
147 
148         /**
149          * Returns the number of rows before {@param adapterPosition}, including this position
150          * which should not be counted towards the collection info.
151          */
getRowsNotForAccessibility(int adapterPosition)152         private int getRowsNotForAccessibility(int adapterPosition) {
153             List<AdapterItem> items = mApps.getAdapterItems();
154             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
155             int extraRows = 0;
156             for (int i = 0; i <= adapterPosition; i++) {
157                 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
158                     extraRows++;
159                 }
160             }
161             return extraRows;
162         }
163 
164         @Override
getPaddingBottom()165         public int getPaddingBottom() {
166             return mLauncher.getDragLayer().getInsets().bottom;
167         }
168     }
169 
170     /**
171      * Helper class to size the grid items.
172      */
173     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
174 
GridSpanSizer()175         public GridSpanSizer() {
176             super();
177             setSpanIndexCacheEnabled(true);
178         }
179 
180         @Override
getSpanSize(int position)181         public int getSpanSize(int position) {
182             if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
183                 return 1;
184             } else {
185                     // Section breaks span the full width
186                     return mAppsPerRow;
187             }
188         }
189     }
190 
191     private final Launcher mLauncher;
192     private final LayoutInflater mLayoutInflater;
193     private final AlphabeticalAppsList mApps;
194     private final GridLayoutManager mGridLayoutMgr;
195     private final GridSpanSizer mGridSizer;
196     private final View.OnClickListener mIconClickListener;
197     private final View.OnLongClickListener mIconLongClickListener;
198 
199     private int mAppsPerRow;
200 
201     private BindViewCallback mBindViewCallback;
202     private AllAppsSearchBarController mSearchController;
203     private OnFocusChangeListener mIconFocusListener;
204 
205     // The text to show when there are no search results and no market search handler.
206     private String mEmptySearchMessage;
207     // The intent to send off to the market app, updated each time the search query changes.
208     private Intent mMarketSearchIntent;
209 
AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener)210     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
211             iconClickListener, View.OnLongClickListener iconLongClickListener) {
212         Resources res = launcher.getResources();
213         mLauncher = launcher;
214         mApps = apps;
215         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
216         mGridSizer = new GridSpanSizer();
217         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
218         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
219         mLayoutInflater = LayoutInflater.from(launcher);
220         mIconClickListener = iconClickListener;
221         mIconLongClickListener = iconLongClickListener;
222     }
223 
isDividerViewType(int viewType)224     public static boolean isDividerViewType(int viewType) {
225         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
226     }
227 
isIconViewType(int viewType)228     public static boolean isIconViewType(int viewType) {
229         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
230     }
231 
isViewType(int viewType, int viewTypeMask)232     public static boolean isViewType(int viewType, int viewTypeMask) {
233         return (viewType & viewTypeMask) != 0;
234     }
235 
236     /**
237      * Sets the number of apps per row.
238      */
setNumAppsPerRow(int appsPerRow)239     public void setNumAppsPerRow(int appsPerRow) {
240         mAppsPerRow = appsPerRow;
241         mGridLayoutMgr.setSpanCount(appsPerRow);
242     }
243 
setSearchController(AllAppsSearchBarController searchController)244     public void setSearchController(AllAppsSearchBarController searchController) {
245         mSearchController = searchController;
246     }
247 
setIconFocusListener(OnFocusChangeListener focusListener)248     public void setIconFocusListener(OnFocusChangeListener focusListener) {
249         mIconFocusListener = focusListener;
250     }
251 
252     /**
253      * Sets the last search query that was made, used to show when there are no results and to also
254      * seed the intent for searching the market.
255      */
setLastSearchQuery(String query)256     public void setLastSearchQuery(String query) {
257         Resources res = mLauncher.getResources();
258         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
259         mMarketSearchIntent = mSearchController.createMarketSearchIntent(query);
260     }
261 
262     /**
263      * Sets the callback for when views are bound.
264      */
setBindViewCallback(BindViewCallback cb)265     public void setBindViewCallback(BindViewCallback cb) {
266         mBindViewCallback = cb;
267     }
268 
269     /**
270      * Returns the grid layout manager.
271      */
getLayoutManager()272     public GridLayoutManager getLayoutManager() {
273         return mGridLayoutMgr;
274     }
275 
276     @Override
onCreateViewHolder(ViewGroup parent, int viewType)277     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
278         switch (viewType) {
279             case VIEW_TYPE_ICON:
280             case VIEW_TYPE_PREDICTION_ICON:
281                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
282                         R.layout.all_apps_icon, parent, false);
283                 icon.setOnClickListener(mIconClickListener);
284                 icon.setOnLongClickListener(mIconLongClickListener);
285                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
286                         .getLongPressTimeout());
287                 icon.setOnFocusChangeListener(mIconFocusListener);
288 
289                 // Ensure the all apps icon height matches the workspace icons
290                 icon.getLayoutParams().height = getCellSize().y;
291                 return new ViewHolder(icon);
292             case VIEW_TYPE_DISCOVERY_ITEM:
293                 AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
294                         .inflate(R.layout.all_apps_discovery_item, parent, false);
295                 appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
296                         mIconLongClickListener);
297                 return new ViewHolder(appDiscoveryItemView);
298             case VIEW_TYPE_EMPTY_SEARCH:
299                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
300                         parent, false));
301             case VIEW_TYPE_SEARCH_MARKET:
302                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
303                         parent, false);
304                 searchMarketView.setOnClickListener(new View.OnClickListener() {
305                     @Override
306                     public void onClick(View v) {
307                         mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
308                     }
309                 });
310                 return new ViewHolder(searchMarketView);
311             case VIEW_TYPE_SEARCH_DIVIDER:
312                 return new ViewHolder(mLayoutInflater.inflate(
313                         R.layout.all_apps_search_divider, parent, false));
314             case VIEW_TYPE_APPS_LOADING_DIVIDER:
315                 View loadingDividerView = mLayoutInflater.inflate(
316                         R.layout.all_apps_discovery_loading_divider, parent, false);
317                 return new ViewHolder(loadingDividerView);
318             case VIEW_TYPE_PREDICTION_DIVIDER:
319             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
320                 return new ViewHolder(mLayoutInflater.inflate(
321                         R.layout.all_apps_divider, parent, false));
322             default:
323                 throw new RuntimeException("Unexpected view type");
324         }
325     }
326 
getCellSize()327     private Point getCellSize() {
328         return mLauncher.getDeviceProfile().getCellSize();
329     }
330 
331     @Override
onBindViewHolder(ViewHolder holder, int position)332     public void onBindViewHolder(ViewHolder holder, int position) {
333         switch (holder.getItemViewType()) {
334             case VIEW_TYPE_ICON:
335             case VIEW_TYPE_PREDICTION_ICON:
336                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
337                 BubbleTextView icon = (BubbleTextView) holder.itemView;
338                 icon.applyFromApplicationInfo(info);
339                 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
340                 break;
341             case VIEW_TYPE_DISCOVERY_ITEM:
342                 AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
343                         mApps.getAdapterItems().get(position).appInfo;
344                 AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView;
345                 view.apply(appDiscoveryAppInfo);
346                 break;
347             case VIEW_TYPE_EMPTY_SEARCH:
348                 TextView emptyViewText = (TextView) holder.itemView;
349                 emptyViewText.setText(mEmptySearchMessage);
350                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
351                         Gravity.START | Gravity.CENTER_VERTICAL);
352                 break;
353             case VIEW_TYPE_SEARCH_MARKET:
354                 TextView searchView = (TextView) holder.itemView;
355                 if (mMarketSearchIntent != null) {
356                     searchView.setVisibility(View.VISIBLE);
357                 } else {
358                     searchView.setVisibility(View.GONE);
359                 }
360                 break;
361             case VIEW_TYPE_APPS_LOADING_DIVIDER:
362                 int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
363                 int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
364                 holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading);
365                 holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded);
366                 break;
367             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
368                 // nothing to do
369                 break;
370         }
371         if (mBindViewCallback != null) {
372             mBindViewCallback.onBindView(holder);
373         }
374     }
375 
376     @Override
onFailedToRecycleView(ViewHolder holder)377     public boolean onFailedToRecycleView(ViewHolder holder) {
378         // Always recycle and we will reset the view when it is bound
379         return true;
380     }
381 
382     @Override
getItemCount()383     public int getItemCount() {
384         return mApps.getAdapterItems().size();
385     }
386 
387     @Override
getItemViewType(int position)388     public int getItemViewType(int position) {
389         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
390         return item.viewType;
391     }
392 }
393