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