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