• 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 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 final 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         mAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns;
204         mGridLayoutMgr.setSpanCount(mAppsPerRow);
205     }
206 
isDividerViewType(int viewType)207     public static boolean isDividerViewType(int viewType) {
208         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
209     }
210 
isIconViewType(int viewType)211     public static boolean isIconViewType(int viewType) {
212         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
213     }
214 
isViewType(int viewType, int viewTypeMask)215     public static boolean isViewType(int viewType, int viewTypeMask) {
216         return (viewType & viewTypeMask) != 0;
217     }
218 
setIconFocusListener(OnFocusChangeListener focusListener)219     public void setIconFocusListener(OnFocusChangeListener focusListener) {
220         mIconFocusListener = focusListener;
221     }
222 
223     /**
224      * Sets the last search query that was made, used to show when there are no results and to also
225      * seed the intent for searching the market.
226      */
setLastSearchQuery(String query)227     public void setLastSearchQuery(String query) {
228         Resources res = mLauncher.getResources();
229         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
230         mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
231     }
232 
233     /**
234      * Sets the callback for when views are bound.
235      */
setBindViewCallback(BindViewCallback cb)236     public void setBindViewCallback(BindViewCallback cb) {
237         mBindViewCallback = cb;
238     }
239 
240     /**
241      * Returns the grid layout manager.
242      */
getLayoutManager()243     public GridLayoutManager getLayoutManager() {
244         return mGridLayoutMgr;
245     }
246 
247     @Override
onCreateViewHolder(ViewGroup parent, int viewType)248     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
249         switch (viewType) {
250             case VIEW_TYPE_ICON:
251                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
252                         R.layout.all_apps_icon, parent, false);
253                 icon.setOnClickListener(ItemClickHandler.INSTANCE);
254                 icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS);
255                 icon.setLongPressTimeoutFactor(1f);
256                 icon.setOnFocusChangeListener(mIconFocusListener);
257 
258                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
259                 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
260                 return new ViewHolder(icon);
261             case VIEW_TYPE_EMPTY_SEARCH:
262                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
263                         parent, false));
264             case VIEW_TYPE_SEARCH_MARKET:
265                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
266                         parent, false);
267                 searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely(
268                         v, mMarketSearchIntent, null, AppLaunchTracker.CONTAINER_SEARCH));
269                 return new ViewHolder(searchMarketView);
270             case VIEW_TYPE_ALL_APPS_DIVIDER:
271                 return new ViewHolder(mLayoutInflater.inflate(
272                         R.layout.all_apps_divider, parent, false));
273             case VIEW_TYPE_WORK_TAB_FOOTER:
274                 View footer = mLayoutInflater.inflate(R.layout.work_tab_footer, parent, false);
275                 return new ViewHolder(footer);
276             default:
277                 throw new RuntimeException("Unexpected view type");
278         }
279     }
280 
281     @Override
onBindViewHolder(ViewHolder holder, int position)282     public void onBindViewHolder(ViewHolder holder, int position) {
283         switch (holder.getItemViewType()) {
284             case VIEW_TYPE_ICON:
285                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
286                 BubbleTextView icon = (BubbleTextView) holder.itemView;
287                 icon.reset();
288                 icon.applyFromApplicationInfo(info);
289                 break;
290             case VIEW_TYPE_EMPTY_SEARCH:
291                 TextView emptyViewText = (TextView) holder.itemView;
292                 emptyViewText.setText(mEmptySearchMessage);
293                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
294                         Gravity.START | Gravity.CENTER_VERTICAL);
295                 break;
296             case VIEW_TYPE_SEARCH_MARKET:
297                 TextView searchView = (TextView) holder.itemView;
298                 if (mMarketSearchIntent != null) {
299                     searchView.setVisibility(View.VISIBLE);
300                 } else {
301                     searchView.setVisibility(View.GONE);
302                 }
303                 break;
304             case VIEW_TYPE_ALL_APPS_DIVIDER:
305                 // nothing to do
306                 break;
307             case VIEW_TYPE_WORK_TAB_FOOTER:
308                 WorkModeSwitch workModeToggle = holder.itemView.findViewById(R.id.work_mode_toggle);
309                 workModeToggle.refresh();
310                 TextView managedByLabel = holder.itemView.findViewById(R.id.managed_by_label);
311                 boolean anyProfileQuietModeEnabled = UserManagerCompat.getInstance(
312                         managedByLabel.getContext()).isAnyProfileQuietModeEnabled();
313                 managedByLabel.setText(anyProfileQuietModeEnabled
314                         ? R.string.work_mode_off_label : R.string.work_mode_on_label);
315                 break;
316         }
317         if (mBindViewCallback != null) {
318             mBindViewCallback.onBindView(holder);
319         }
320     }
321 
322     @Override
onFailedToRecycleView(ViewHolder holder)323     public boolean onFailedToRecycleView(ViewHolder holder) {
324         // Always recycle and we will reset the view when it is bound
325         return true;
326     }
327 
328     @Override
getItemCount()329     public int getItemCount() {
330         return mApps.getAdapterItems().size();
331     }
332 
333     @Override
getItemViewType(int position)334     public int getItemViewType(int position) {
335         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
336         return item.viewType;
337     }
338 
339 }
340