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.res.Resources;
21import android.support.animation.DynamicAnimation;
22import android.support.animation.SpringAnimation;
23import android.support.v4.view.accessibility.AccessibilityEventCompat;
24import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
25import android.support.v4.view.accessibility.AccessibilityRecordCompat;
26import android.support.v7.widget.GridLayoutManager;
27import android.support.v7.widget.RecyclerView;
28import android.view.Gravity;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.View.OnFocusChangeListener;
32import android.view.ViewConfiguration;
33import android.view.ViewGroup;
34import android.view.accessibility.AccessibilityEvent;
35import android.widget.TextView;
36
37import com.android.launcher3.AppInfo;
38import com.android.launcher3.BubbleTextView;
39import com.android.launcher3.Launcher;
40import com.android.launcher3.R;
41import com.android.launcher3.Utilities;
42import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
43import com.android.launcher3.anim.SpringAnimationHandler;
44import com.android.launcher3.config.FeatureFlags;
45import com.android.launcher3.discovery.AppDiscoveryAppInfo;
46import com.android.launcher3.discovery.AppDiscoveryItemView;
47import com.android.launcher3.util.PackageManagerHelper;
48
49import java.util.List;
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
58    // A normal icon
59    public static final int VIEW_TYPE_ICON = 1 << 1;
60    // A prediction icon
61    public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2;
62    // The message shown when there are no filtered results
63    public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3;
64    // The message to continue to a market search when there are no filtered results
65    public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4;
66
67    // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
68    // but differ in enough attributes to require different view types
69
70    // A divider that separates the apps list and the search market button
71    public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5;
72    // The divider that separates prediction icons from the app list
73    public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 6;
74    public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 7;
75    public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 8;
76
77    // Common view type masks
78    public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_MARKET_DIVIDER
79            | VIEW_TYPE_PREDICTION_DIVIDER;
80    public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
81            | VIEW_TYPE_PREDICTION_ICON;
82    public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
83            | VIEW_TYPE_DISCOVERY_ITEM;
84    public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON
85            | VIEW_TYPE_PREDICTION_DIVIDER;
86
87
88    public interface BindViewCallback {
89        void onBindView(ViewHolder holder);
90    }
91
92    /**
93     * ViewHolder for each icon.
94     */
95    public static class ViewHolder extends RecyclerView.ViewHolder {
96
97        public ViewHolder(View v) {
98            super(v);
99        }
100    }
101
102    /**
103     * A subclass of GridLayoutManager that overrides accessibility values during app search.
104     */
105    public class AppsGridLayoutManager extends GridLayoutManager {
106
107        public AppsGridLayoutManager(Context context) {
108            super(context, 1, GridLayoutManager.VERTICAL, false);
109        }
110
111        @Override
112        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
113            super.onInitializeAccessibilityEvent(event);
114
115            // Ensure that we only report the number apps for accessibility not including other
116            // adapter views
117            final AccessibilityRecordCompat record = AccessibilityEventCompat
118                    .asRecord(event);
119            record.setItemCount(mApps.getNumFilteredApps());
120            record.setFromIndex(Math.max(0,
121                    record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
122            record.setToIndex(Math.max(0,
123                    record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
124        }
125
126        @Override
127        public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
128                RecyclerView.State state) {
129            return super.getRowCountForAccessibility(recycler, state) -
130                    getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
131        }
132
133        @Override
134        public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
135                RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
136            super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
137
138            ViewGroup.LayoutParams lp = host.getLayoutParams();
139            AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
140            if (!(lp instanceof LayoutParams) || (cic == null)) {
141                return;
142            }
143            LayoutParams glp = (LayoutParams) lp;
144            info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
145                    cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
146                    cic.getRowSpan(),
147                    cic.getColumnIndex(),
148                    cic.getColumnSpan(),
149                    cic.isHeading(),
150                    cic.isSelected()));
151        }
152
153        /**
154         * Returns the number of rows before {@param adapterPosition}, including this position
155         * which should not be counted towards the collection info.
156         */
157        private int getRowsNotForAccessibility(int adapterPosition) {
158            List<AdapterItem> items = mApps.getAdapterItems();
159            adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
160            int extraRows = 0;
161            for (int i = 0; i <= adapterPosition; i++) {
162                if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
163                    extraRows++;
164                }
165            }
166            return extraRows;
167        }
168    }
169
170    /**
171     * Helper class to size the grid items.
172     */
173    public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
174
175        public GridSpanSizer() {
176            super();
177            setSpanIndexCacheEnabled(true);
178        }
179
180        @Override
181        public int getSpanSize(int position) {
182            if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
183                return 1;
184            } else {
185                    // Section breaks span the full width
186                    return mAppsPerRow;
187            }
188        }
189    }
190
191    private final Launcher mLauncher;
192    private final LayoutInflater mLayoutInflater;
193    private final AlphabeticalAppsList mApps;
194    private final GridLayoutManager mGridLayoutMgr;
195    private final GridSpanSizer mGridSizer;
196    private final View.OnClickListener mIconClickListener;
197    private final View.OnLongClickListener mIconLongClickListener;
198
199    private int mAppsPerRow;
200
201    private BindViewCallback mBindViewCallback;
202    private OnFocusChangeListener mIconFocusListener;
203
204    // The text to show when there are no search results and no market search handler.
205    private String mEmptySearchMessage;
206    // The intent to send off to the market app, updated each time the search query changes.
207    private Intent mMarketSearchIntent;
208
209    private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
210
211    public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
212            iconClickListener, View.OnLongClickListener iconLongClickListener) {
213        Resources res = launcher.getResources();
214        mLauncher = launcher;
215        mApps = apps;
216        mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
217        mGridSizer = new GridSpanSizer();
218        mGridLayoutMgr = new AppsGridLayoutManager(launcher);
219        mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
220        mLayoutInflater = LayoutInflater.from(launcher);
221        mIconClickListener = iconClickListener;
222        mIconLongClickListener = iconLongClickListener;
223        if (FeatureFlags.LAUNCHER3_PHYSICS) {
224            mSpringAnimationHandler = new SpringAnimationHandler<>(
225                    SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory());
226        }
227    }
228
229    public SpringAnimationHandler getSpringAnimationHandler() {
230        return mSpringAnimationHandler;
231    }
232
233    public static boolean isDividerViewType(int viewType) {
234        return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
235    }
236
237    public static boolean isIconViewType(int viewType) {
238        return isViewType(viewType, VIEW_TYPE_MASK_ICON);
239    }
240
241    public static boolean isViewType(int viewType, int viewTypeMask) {
242        return (viewType & viewTypeMask) != 0;
243    }
244
245    /**
246     * Sets the number of apps per row.
247     */
248    public void setNumAppsPerRow(int appsPerRow) {
249        mAppsPerRow = appsPerRow;
250        mGridLayoutMgr.setSpanCount(appsPerRow);
251    }
252
253    public int getNumAppsPerRow() {
254        return mAppsPerRow;
255    }
256
257    public void setIconFocusListener(OnFocusChangeListener focusListener) {
258        mIconFocusListener = focusListener;
259    }
260
261    /**
262     * Sets the last search query that was made, used to show when there are no results and to also
263     * seed the intent for searching the market.
264     */
265    public void setLastSearchQuery(String query) {
266        Resources res = mLauncher.getResources();
267        mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
268        mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
269    }
270
271    /**
272     * Sets the callback for when views are bound.
273     */
274    public void setBindViewCallback(BindViewCallback cb) {
275        mBindViewCallback = cb;
276    }
277
278    /**
279     * Returns the grid layout manager.
280     */
281    public GridLayoutManager getLayoutManager() {
282        return mGridLayoutMgr;
283    }
284
285    @Override
286    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
287        switch (viewType) {
288            case VIEW_TYPE_ICON:
289            case VIEW_TYPE_PREDICTION_ICON:
290                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
291                        R.layout.all_apps_icon, parent, false);
292                icon.setOnClickListener(mIconClickListener);
293                icon.setOnLongClickListener(mIconLongClickListener);
294                icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
295                icon.setOnFocusChangeListener(mIconFocusListener);
296
297                // Ensure the all apps icon height matches the workspace icons in portrait mode.
298                icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
299                return new ViewHolder(icon);
300            case VIEW_TYPE_DISCOVERY_ITEM:
301                AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
302                        .inflate(R.layout.all_apps_discovery_item, parent, false);
303                appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
304                        mIconLongClickListener);
305                return new ViewHolder(appDiscoveryItemView);
306            case VIEW_TYPE_EMPTY_SEARCH:
307                return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
308                        parent, false));
309            case VIEW_TYPE_SEARCH_MARKET:
310                View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
311                        parent, false);
312                searchMarketView.setOnClickListener(new View.OnClickListener() {
313                    @Override
314                    public void onClick(View v) {
315                        mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
316                    }
317                });
318                return new ViewHolder(searchMarketView);
319            case VIEW_TYPE_APPS_LOADING_DIVIDER:
320                View loadingDividerView = mLayoutInflater.inflate(
321                        R.layout.all_apps_discovery_loading_divider, parent, false);
322                return new ViewHolder(loadingDividerView);
323            case VIEW_TYPE_PREDICTION_DIVIDER:
324            case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
325                return new ViewHolder(mLayoutInflater.inflate(
326                        R.layout.all_apps_divider, parent, false));
327            default:
328                throw new RuntimeException("Unexpected view type");
329        }
330    }
331
332    @Override
333    public void onBindViewHolder(ViewHolder holder, int position) {
334        switch (holder.getItemViewType()) {
335            case VIEW_TYPE_ICON:
336            case VIEW_TYPE_PREDICTION_ICON:
337                AppInfo info = mApps.getAdapterItems().get(position).appInfo;
338                BubbleTextView icon = (BubbleTextView) holder.itemView;
339                icon.applyFromApplicationInfo(info);
340                break;
341            case VIEW_TYPE_DISCOVERY_ITEM:
342                AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
343                        mApps.getAdapterItems().get(position).appInfo;
344                AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView;
345                view.apply(appDiscoveryAppInfo);
346                break;
347            case VIEW_TYPE_EMPTY_SEARCH:
348                TextView emptyViewText = (TextView) holder.itemView;
349                emptyViewText.setText(mEmptySearchMessage);
350                emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
351                        Gravity.START | Gravity.CENTER_VERTICAL);
352                break;
353            case VIEW_TYPE_SEARCH_MARKET:
354                TextView searchView = (TextView) holder.itemView;
355                if (mMarketSearchIntent != null) {
356                    searchView.setVisibility(View.VISIBLE);
357                } else {
358                    searchView.setVisibility(View.GONE);
359                }
360                break;
361            case VIEW_TYPE_APPS_LOADING_DIVIDER:
362                int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
363                int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
364                holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading);
365                holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded);
366                break;
367            case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
368                // nothing to do
369                break;
370        }
371        if (mBindViewCallback != null) {
372            mBindViewCallback.onBindView(holder);
373        }
374    }
375
376    @Override
377    public void onViewAttachedToWindow(ViewHolder holder) {
378        int type = holder.getItemViewType();
379        if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
380            mSpringAnimationHandler.add(holder.itemView, holder);
381        }
382    }
383
384    @Override
385    public void onViewDetachedFromWindow(ViewHolder holder) {
386        int type = holder.getItemViewType();
387        if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
388            mSpringAnimationHandler.remove(holder.itemView);
389        }
390    }
391
392    @Override
393    public boolean onFailedToRecycleView(ViewHolder holder) {
394        // Always recycle and we will reset the view when it is bound
395        return true;
396    }
397
398    @Override
399    public int getItemCount() {
400        return mApps.getAdapterItems().size();
401    }
402
403    @Override
404    public int getItemViewType(int position) {
405        AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
406        return item.viewType;
407    }
408
409    /**
410     * Helper class to set the SpringAnimation values for an item in the adapter.
411     */
412    private class AllAppsSpringAnimationFactory
413            implements SpringAnimationHandler.AnimationFactory<ViewHolder> {
414        private static final float DEFAULT_MAX_VALUE_PX = 100;
415        private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX;
416
417        // Damping ratio range is [0, 1]
418        private static final float SPRING_DAMPING_RATIO = 0.55f;
419
420        // Stiffness is a non-negative number.
421        private static final float MIN_SPRING_STIFFNESS = 580f;
422        private static final float MAX_SPRING_STIFFNESS = 900f;
423
424        // The amount by which each adjacent rows' stiffness will differ.
425        private static final float ROW_STIFFNESS_COEFFICIENT = 50f;
426
427        @Override
428        public SpringAnimation initialize(ViewHolder vh) {
429            return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0);
430        }
431
432        /**
433         * @param spring A new or recycled SpringAnimation.
434         * @param vh The ViewHolder that {@param spring} is related to.
435         */
436        @Override
437        public void update(SpringAnimation spring, ViewHolder vh) {
438            int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size());
439            int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps,
440                    mAppsPerRow);
441
442            int col = appPosition % mAppsPerRow;
443            int row = appPosition / mAppsPerRow;
444
445            int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count
446            if (row > (numTotalRows / 2)) {
447                // Mirror the rows so that the top row acts the same as the bottom row.
448                row = Math.abs(numTotalRows - row);
449            }
450
451            calculateSpringValues(spring, row, col);
452        }
453
454        @Override
455        public void setDefaultValues(SpringAnimation spring) {
456            calculateSpringValues(spring, 0, mAppsPerRow / 2);
457        }
458
459        /**
460         * We manipulate the stiffness, min, and max values based on the items distance to the
461         * first row and the items distance to the center column to create the ^-shaped motion
462         * effect.
463         */
464        private void calculateSpringValues(SpringAnimation spring, int row, int col) {
465            float rowFactor = (1 + row) * 0.5f;
466            float colFactor = getColumnFactor(col, mAppsPerRow);
467
468            float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor);
469            float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor);
470
471            float stiffness = Utilities.boundToRange(
472                    MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT),
473                    MIN_SPRING_STIFFNESS,
474                    MAX_SPRING_STIFFNESS);
475
476            spring.setMinValue(minValue)
477                    .setMaxValue(maxValue)
478                    .getSpring()
479                    .setStiffness(stiffness)
480                    .setDampingRatio(SPRING_DAMPING_RATIO);
481        }
482
483        /**
484         * @return The app position is the position of the app in the Adapter if we ignored all
485         * other view types.
486         *
487         * The first app is at position 0, and the first app each following row is at a
488         * position that is a multiple of {@param appsPerRow}.
489         *
490         * ie. If there are 5 apps per row, and there are two rows of apps:
491         *     0 1 2 3 4
492         *     5 6 7 8 9
493         */
494        private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
495            if (position < numPredictedApps) {
496                // Predicted apps are first in the adapter.
497                return position;
498            }
499
500            // There is at most 1 divider view between the predicted apps and the alphabetical apps.
501            int numDividerViews = numPredictedApps == 0 ? 0 : 1;
502
503            // This offset takes into consideration an incomplete row of predicted apps.
504            int numPredictedAppsOffset = appsPerRow - numPredictedApps;
505            return position + numPredictedAppsOffset - numDividerViews;
506        }
507
508        /**
509         * Increase the column factor as the distance increases between the column and the center
510         * column(s).
511         */
512        private float getColumnFactor(int col, int numCols) {
513            float centerColumn = numCols / 2;
514            int distanceToCenter = (int) Math.abs(col - centerColumn);
515
516            boolean evenNumberOfColumns = numCols % 2 == 0;
517            if (evenNumberOfColumns && col < centerColumn) {
518                distanceToCenter -= 1;
519            }
520
521            float factor = 0;
522            while (distanceToCenter > 0) {
523                if (distanceToCenter == 1) {
524                    factor += 0.2f;
525                } else {
526                    factor += 0.1f;
527                }
528                --distanceToCenter;
529            }
530
531            return factor;
532        }
533    }
534}
535