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