• 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.pm.PackageManager;
21 import android.content.pm.ResolveInfo;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.Paint;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.graphics.drawable.Drawable;
28 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
29 import android.support.v4.view.accessibility.AccessibilityEventCompat;
30 import android.net.Uri;
31 import android.support.v7.widget.GridLayoutManager;
32 import android.support.v7.widget.RecyclerView;
33 import android.view.Gravity;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.widget.TextView;
40 import com.android.launcher3.AppInfo;
41 import com.android.launcher3.BubbleTextView;
42 import com.android.launcher3.Launcher;
43 import com.android.launcher3.R;
44 import com.android.launcher3.Utilities;
45 import com.android.launcher3.util.Thunk;
46 
47 import java.util.HashMap;
48 import java.util.List;
49 
50 
51 /**
52  * The grid view adapter of all the apps.
53  */
54 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
55 
56     public static final String TAG = "AppsGridAdapter";
57     private static final boolean DEBUG = false;
58 
59     // A section break in the grid
60     public static final int SECTION_BREAK_VIEW_TYPE = 0;
61     // A normal icon
62     public static final int ICON_VIEW_TYPE = 1;
63     // A prediction icon
64     public static final int PREDICTION_ICON_VIEW_TYPE = 2;
65     // The message shown when there are no filtered results
66     public static final int EMPTY_SEARCH_VIEW_TYPE = 3;
67     // A divider that separates the apps list and the search market button
68     public static final int SEARCH_MARKET_DIVIDER_VIEW_TYPE = 4;
69     // The message to continue to a market search when there are no filtered results
70     public static final int SEARCH_MARKET_VIEW_TYPE = 5;
71 
72     /**
73      * ViewHolder for each icon.
74      */
75     public static class ViewHolder extends RecyclerView.ViewHolder {
76         public View mContent;
77 
ViewHolder(View v)78         public ViewHolder(View v) {
79             super(v);
80             mContent = 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         }
103 
104         @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)105         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
106                 RecyclerView.State state) {
107             if (mApps.hasNoFilteredResults()) {
108                 // Disregard the no-search-results text as a list item for accessibility
109                 return 0;
110             } else {
111                 return super.getRowCountForAccessibility(recycler, state);
112             }
113         }
114     }
115 
116     /**
117      * Helper class to size the grid items.
118      */
119     public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
120 
GridSpanSizer()121         public GridSpanSizer() {
122             super();
123             setSpanIndexCacheEnabled(true);
124         }
125 
126         @Override
getSpanSize(int position)127         public int getSpanSize(int position) {
128             switch (mApps.getAdapterItems().get(position).viewType) {
129                 case AllAppsGridAdapter.ICON_VIEW_TYPE:
130                 case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE:
131                     return 1;
132                 default:
133                     // Section breaks span the full width
134                     return mAppsPerRow;
135             }
136         }
137     }
138 
139     /**
140      * Helper class to draw the section headers
141      */
142     public class GridItemDecoration extends RecyclerView.ItemDecoration {
143 
144         private static final boolean DEBUG_SECTION_MARGIN = false;
145         private static final boolean FADE_OUT_SECTIONS = false;
146 
147         private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>();
148         private Rect mTmpBounds = new Rect();
149 
150         @Override
onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)151         public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
152             if (mApps.hasFilter() || mAppsPerRow == 0) {
153                 return;
154             }
155 
156             if (DEBUG_SECTION_MARGIN) {
157                 Paint p = new Paint();
158                 p.setColor(0x33ff0000);
159                 c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin,
160                         parent.getMeasuredHeight(), p);
161             }
162 
163             List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
164             boolean hasDrawnPredictedAppsDivider = false;
165             boolean showSectionNames = mSectionNamesMargin > 0;
166             int childCount = parent.getChildCount();
167             int lastSectionTop = 0;
168             int lastSectionHeight = 0;
169             for (int i = 0; i < childCount; i++) {
170                 View child = parent.getChildAt(i);
171                 ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
172                 if (!isValidHolderAndChild(holder, child, items)) {
173                     continue;
174                 }
175 
176                 if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) {
177                     // Draw the divider under the predicted apps
178                     int top = child.getTop() + child.getHeight() + mPredictionBarDividerOffset;
179                     c.drawLine(mBackgroundPadding.left, top,
180                             parent.getWidth() - mBackgroundPadding.right, top,
181                             mPredictedAppsDividerPaint);
182                     hasDrawnPredictedAppsDivider = true;
183 
184                 } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) {
185                     // At this point, we only draw sections for each section break;
186                     int viewTopOffset = (2 * child.getPaddingTop());
187                     int pos = holder.getPosition();
188                     AlphabeticalAppsList.AdapterItem item = items.get(pos);
189                     AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo;
190 
191                     // Draw all the sections for this index
192                     String lastSectionName = item.sectionName;
193                     for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) {
194                         AlphabeticalAppsList.AdapterItem nextItem = items.get(pos);
195                         String sectionName = nextItem.sectionName;
196                         if (nextItem.sectionInfo != sectionInfo) {
197                             break;
198                         }
199                         if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) {
200                             continue;
201                         }
202 
203 
204                         // Find the section name bounds
205                         PointF sectionBounds = getAndCacheSectionBounds(sectionName);
206 
207                         // Calculate where to draw the section
208                         int sectionBaseline = (int) (viewTopOffset + sectionBounds.y);
209                         int x = mIsRtl ?
210                                 parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin :
211                                         mBackgroundPadding.left;
212                         x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f);
213                         int y = child.getTop() + sectionBaseline;
214 
215                         // Determine whether this is the last row with apps in that section, if
216                         // so, then fix the section to the row allowing it to scroll past the
217                         // baseline, otherwise, bound it to the baseline so it's in the viewport
218                         int appIndexInSection = items.get(pos).sectionAppIndex;
219                         int nextRowPos = Math.min(items.size() - 1,
220                                 pos + mAppsPerRow - (appIndexInSection % mAppsPerRow));
221                         AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos);
222                         boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName);
223                         if (!fixedToRow) {
224                             y = Math.max(sectionBaseline, y);
225                         }
226 
227                         // In addition, if it overlaps with the last section that was drawn, then
228                         // offset it so that it does not overlap
229                         if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) {
230                             y += lastSectionTop - y + lastSectionHeight;
231                         }
232 
233                         // Draw the section header
234                         if (FADE_OUT_SECTIONS) {
235                             int alpha = 255;
236                             if (fixedToRow) {
237                                 alpha = Math.min(255,
238                                         (int) (255 * (Math.max(0, y) / (float) sectionBaseline)));
239                             }
240                             mSectionTextPaint.setAlpha(alpha);
241                         }
242                         c.drawText(sectionName, x, y, mSectionTextPaint);
243 
244                         lastSectionTop = y;
245                         lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset);
246                         lastSectionName = sectionName;
247                     }
248                     i += (sectionInfo.numApps - item.sectionAppIndex);
249                 }
250             }
251         }
252 
253         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)254         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
255                 RecyclerView.State state) {
256             // Do nothing
257         }
258 
259         /**
260          * Given a section name, return the bounds of the given section name.
261          */
getAndCacheSectionBounds(String sectionName)262         private PointF getAndCacheSectionBounds(String sectionName) {
263             PointF bounds = mCachedSectionBounds.get(sectionName);
264             if (bounds == null) {
265                 mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds);
266                 bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height());
267                 mCachedSectionBounds.put(sectionName, bounds);
268             }
269             return bounds;
270         }
271 
272         /**
273          * Returns whether we consider this a valid view holder for us to draw a divider or section for.
274          */
isValidHolderAndChild(ViewHolder holder, View child, List<AlphabeticalAppsList.AdapterItem> items)275         private boolean isValidHolderAndChild(ViewHolder holder, View child,
276                 List<AlphabeticalAppsList.AdapterItem> items) {
277             // Ensure item is not already removed
278             GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
279                     child.getLayoutParams();
280             if (lp.isItemRemoved()) {
281                 return false;
282             }
283             // Ensure we have a valid holder
284             if (holder == null) {
285                 return false;
286             }
287             // Ensure we have a holder position
288             int pos = holder.getPosition();
289             if (pos < 0 || pos >= items.size()) {
290                 return false;
291             }
292             return true;
293         }
294 
295         /**
296          * Returns whether to draw the divider for a given child.
297          */
shouldDrawItemDivider(ViewHolder holder, List<AlphabeticalAppsList.AdapterItem> items)298         private boolean shouldDrawItemDivider(ViewHolder holder,
299                 List<AlphabeticalAppsList.AdapterItem> items) {
300             int pos = holder.getPosition();
301             return items.get(pos).viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
302         }
303 
304         /**
305          * Returns whether to draw the section for the given child.
306          */
shouldDrawItemSection(ViewHolder holder, int childIndex, List<AlphabeticalAppsList.AdapterItem> items)307         private boolean shouldDrawItemSection(ViewHolder holder, int childIndex,
308                 List<AlphabeticalAppsList.AdapterItem> items) {
309             int pos = holder.getPosition();
310             AlphabeticalAppsList.AdapterItem item = items.get(pos);
311 
312             // Ensure it's an icon
313             if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
314                 return false;
315             }
316             // Draw the section header for the first item in each section
317             return (childIndex == 0) ||
318                     (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE);
319         }
320     }
321 
322     private Launcher mLauncher;
323     private LayoutInflater mLayoutInflater;
324     @Thunk AlphabeticalAppsList mApps;
325     private GridLayoutManager mGridLayoutMgr;
326     private GridSpanSizer mGridSizer;
327     private GridItemDecoration mItemDecoration;
328     private View.OnTouchListener mTouchListener;
329     private View.OnClickListener mIconClickListener;
330     private View.OnLongClickListener mIconLongClickListener;
331     @Thunk final Rect mBackgroundPadding = new Rect();
332     @Thunk int mPredictionBarDividerOffset;
333     @Thunk int mAppsPerRow;
334     @Thunk boolean mIsRtl;
335 
336     // The text to show when there are no search results and no market search handler.
337     private String mEmptySearchMessage;
338     // The name of the market app which handles searches, to be used in the format str
339     // below when updating the search-market view.  Only needs to be loaded once.
340     private String mMarketAppName;
341     // The text to show when there is a market app which can handle a specific query, updated
342     // each time the search query changes.
343     private String mMarketSearchMessage;
344     // The intent to send off to the market app, updated each time the search query changes.
345     private Intent mMarketSearchIntent;
346     // The last query that the user entered into the search field
347     private String mLastSearchQuery;
348 
349     // Section drawing
350     @Thunk int mSectionNamesMargin;
351     @Thunk int mSectionHeaderOffset;
352     @Thunk Paint mSectionTextPaint;
353     @Thunk Paint mPredictedAppsDividerPaint;
354 
AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnTouchListener touchListener, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener)355     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps,
356             View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
357             View.OnLongClickListener iconLongClickListener) {
358         Resources res = launcher.getResources();
359         mLauncher = launcher;
360         mApps = apps;
361         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
362         mGridSizer = new GridSpanSizer();
363         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
364         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
365         mItemDecoration = new GridItemDecoration();
366         mLayoutInflater = LayoutInflater.from(launcher);
367         mTouchListener = touchListener;
368         mIconClickListener = iconClickListener;
369         mIconLongClickListener = iconLongClickListener;
370         mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
371         mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset);
372 
373         mSectionTextPaint = new Paint();
374         mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
375                 R.dimen.all_apps_grid_section_text_size));
376         mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color));
377         mSectionTextPaint.setAntiAlias(true);
378 
379         mPredictedAppsDividerPaint = new Paint();
380         mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics()));
381         mPredictedAppsDividerPaint.setColor(0x1E000000);
382         mPredictedAppsDividerPaint.setAntiAlias(true);
383         mPredictionBarDividerOffset =
384                 (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) +
385                         res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2;
386 
387         // Resolve the market app handling additional searches
388         PackageManager pm = launcher.getPackageManager();
389         ResolveInfo marketInfo = pm.resolveActivity(createMarketSearchIntent(""),
390                 PackageManager.MATCH_DEFAULT_ONLY);
391         if (marketInfo != null) {
392             mMarketAppName = marketInfo.loadLabel(pm).toString();
393         }
394     }
395 
396     /**
397      * Sets the number of apps per row.
398      */
setNumAppsPerRow(int appsPerRow)399     public void setNumAppsPerRow(int appsPerRow) {
400         mAppsPerRow = appsPerRow;
401         mGridLayoutMgr.setSpanCount(appsPerRow);
402     }
403 
404     /**
405      * Sets whether we are in RTL mode.
406      */
setRtl(boolean rtl)407     public void setRtl(boolean rtl) {
408         mIsRtl = rtl;
409     }
410 
411     /**
412      * Sets the last search query that was made, used to show when there are no results and to also
413      * seed the intent for searching the market.
414      */
setLastSearchQuery(String query)415     public void setLastSearchQuery(String query) {
416         Resources res = mLauncher.getResources();
417         String formatStr = res.getString(R.string.all_apps_no_search_results);
418         mLastSearchQuery = query;
419         mEmptySearchMessage = String.format(formatStr, query);
420         if (mMarketAppName != null) {
421             mMarketSearchMessage = String.format(res.getString(R.string.all_apps_search_market_message),
422                     mMarketAppName);
423             mMarketSearchIntent = createMarketSearchIntent(query);
424         }
425     }
426 
427     /**
428      * Notifies the adapter of the background padding so that it can draw things correctly in the
429      * item decorator.
430      */
updateBackgroundPadding(Rect padding)431     public void updateBackgroundPadding(Rect padding) {
432         mBackgroundPadding.set(padding);
433     }
434 
435     /**
436      * Returns the grid layout manager.
437      */
getLayoutManager()438     public GridLayoutManager getLayoutManager() {
439         return mGridLayoutMgr;
440     }
441 
442     /**
443      * Returns the item decoration for the recycler view.
444      */
getItemDecoration()445     public RecyclerView.ItemDecoration getItemDecoration() {
446         // We don't draw any headers when we are uncomfortably dense
447         return mItemDecoration;
448     }
449 
450     @Override
onCreateViewHolder(ViewGroup parent, int viewType)451     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
452         switch (viewType) {
453             case SECTION_BREAK_VIEW_TYPE:
454                 return new ViewHolder(new View(parent.getContext()));
455             case ICON_VIEW_TYPE: {
456                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
457                         R.layout.all_apps_icon, parent, false);
458                 icon.setOnTouchListener(mTouchListener);
459                 icon.setOnClickListener(mIconClickListener);
460                 icon.setOnLongClickListener(mIconLongClickListener);
461                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
462                         .getLongPressTimeout());
463                 icon.setFocusable(true);
464                 return new ViewHolder(icon);
465             }
466             case PREDICTION_ICON_VIEW_TYPE: {
467                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
468                         R.layout.all_apps_prediction_bar_icon, parent, false);
469                 icon.setOnTouchListener(mTouchListener);
470                 icon.setOnClickListener(mIconClickListener);
471                 icon.setOnLongClickListener(mIconLongClickListener);
472                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
473                         .getLongPressTimeout());
474                 icon.setFocusable(true);
475                 return new ViewHolder(icon);
476             }
477             case EMPTY_SEARCH_VIEW_TYPE:
478                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
479                         parent, false));
480             case SEARCH_MARKET_DIVIDER_VIEW_TYPE:
481                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_search_market_divider,
482                         parent, false));
483             case SEARCH_MARKET_VIEW_TYPE:
484                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
485                         parent, false);
486                 searchMarketView.setOnClickListener(new View.OnClickListener() {
487                     @Override
488                     public void onClick(View v) {
489                         mLauncher.startSearchFromAllApps(v, mMarketSearchIntent, mLastSearchQuery);
490                     }
491                 });
492                 return new ViewHolder(searchMarketView);
493             default:
494                 throw new RuntimeException("Unexpected view type");
495         }
496     }
497 
498     @Override
onBindViewHolder(ViewHolder holder, int position)499     public void onBindViewHolder(ViewHolder holder, int position) {
500         switch (holder.getItemViewType()) {
501             case ICON_VIEW_TYPE: {
502                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
503                 BubbleTextView icon = (BubbleTextView) holder.mContent;
504                 icon.applyFromApplicationInfo(info);
505                 break;
506             }
507             case PREDICTION_ICON_VIEW_TYPE: {
508                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
509                 BubbleTextView icon = (BubbleTextView) holder.mContent;
510                 icon.applyFromApplicationInfo(info);
511                 break;
512             }
513             case EMPTY_SEARCH_VIEW_TYPE:
514                 TextView emptyViewText = (TextView) holder.mContent;
515                 emptyViewText.setText(mEmptySearchMessage);
516                 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
517                         Gravity.START | Gravity.CENTER_VERTICAL);
518                 break;
519             case SEARCH_MARKET_VIEW_TYPE:
520                 TextView searchView = (TextView) holder.mContent;
521                 if (mMarketSearchIntent != null) {
522                     searchView.setVisibility(View.VISIBLE);
523                     searchView.setContentDescription(mMarketSearchMessage);
524                     searchView.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
525                             Gravity.START | Gravity.CENTER_VERTICAL);
526                     searchView.setText(mMarketSearchMessage);
527                 } else {
528                     searchView.setVisibility(View.GONE);
529                 }
530                 break;
531         }
532     }
533 
534     @Override
getItemCount()535     public int getItemCount() {
536         return mApps.getAdapterItems().size();
537     }
538 
539     @Override
getItemViewType(int position)540     public int getItemViewType(int position) {
541         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
542         return item.viewType;
543     }
544 
545     /**
546      * Creates a new market search intent.
547      */
createMarketSearchIntent(String query)548     private Intent createMarketSearchIntent(String query) {
549         Uri marketSearchUri = Uri.parse("market://search")
550                 .buildUpon()
551                 .appendQueryParameter("q", query)
552                 .build();
553         Intent marketSearchIntent = new Intent(Intent.ACTION_VIEW);
554         marketSearchIntent.setData(marketSearchUri);
555         return marketSearchIntent;
556     }
557 }
558