AllAppsGridAdapter.java revision 3eefff8d77383b9648378ed68aa98064d98be5c3
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.R;
44import com.android.launcher3.Utilities;
45import com.android.launcher3.util.Thunk;
46
47import java.util.HashMap;
48import java.util.List;
49
50
51/**
52 * The grid view adapter of all the apps.
53 */
54public 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
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
89        public AppsGridLayoutManager(Context context) {
90            super(context, 1, GridLayoutManager.VERTICAL, false);
91        }
92
93        @Override
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
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
121        public GridSpanSizer() {
122            super();
123            setSpanIndexCacheEnabled(true);
124        }
125
126        @Override
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
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
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         */
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         */
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         */
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         */
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    @Thunk 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    @Thunk Intent mMarketSearchIntent;
346    // The last query that the user entered into the search field
347    @Thunk String mLastSearchQuery;
348
349    // Section drawing
350    @Thunk int mSectionNamesMargin;
351    @Thunk int mSectionHeaderOffset;
352    @Thunk Paint mSectionTextPaint;
353    @Thunk Paint mPredictedAppsDividerPaint;
354
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     */
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     */
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     */
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     */
431    public void updateBackgroundPadding(Rect padding) {
432        mBackgroundPadding.set(padding);
433    }
434
435    /**
436     * Returns the grid layout manager.
437     */
438    public GridLayoutManager getLayoutManager() {
439        return mGridLayoutMgr;
440    }
441
442    /**
443     * Returns the item decoration for the recycler view.
444     */
445    public RecyclerView.ItemDecoration getItemDecoration() {
446        // We don't draw any headers when we are uncomfortably dense
447        return mItemDecoration;
448    }
449
450    @Override
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
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
535    public int getItemCount() {
536        return mApps.getAdapterItems().size();
537    }
538
539    @Override
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     */
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