Home | History | Annotate | Download | only in allapps
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.launcher3.allapps;
     17 
     18 import android.content.Context;
     19 import android.content.Intent;
     20 import android.content.res.Resources;
     21 import android.graphics.Point;
     22 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     24 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     25 import android.support.v7.widget.GridLayoutManager;
     26 import android.support.v7.widget.RecyclerView;
     27 import android.view.Gravity;
     28 import android.view.LayoutInflater;
     29 import android.view.View;
     30 import android.view.View.OnFocusChangeListener;
     31 import android.view.ViewConfiguration;
     32 import android.view.ViewGroup;
     33 import android.view.accessibility.AccessibilityEvent;
     34 import android.widget.TextView;
     35 
     36 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
     37 import com.android.launcher3.AppInfo;
     38 import com.android.launcher3.BubbleTextView;
     39 import com.android.launcher3.Launcher;
     40 import com.android.launcher3.R;
     41 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
     42 import com.android.launcher3.discovery.AppDiscoveryItemView;
     43 
     44 import java.util.List;
     45 
     46 /**
     47  * The grid view adapter of all the apps.
     48  */
     49 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
     50 
     51     public static final String TAG = "AppsGridAdapter";
     52 
     53     // A normal icon
     54     public static final int VIEW_TYPE_ICON = 1 << 1;
     55     // A prediction icon
     56     public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2;
     57     // The message shown when there are no filtered results
     58     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3;
     59     // The message to continue to a market search when there are no filtered results
     60     public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4;
     61 
     62     // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
     63     // but differ in enough attributes to require different view types
     64 
     65     // A divider that separates the apps list and the search market button
     66     public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5;
     67     // The divider under the search field
     68     public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6;
     69     // The divider that separates prediction icons from the app list
     70     public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7;
     71     public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 8;
     72     public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 9;
     73 
     74     // Common view type masks
     75     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER
     76             | VIEW_TYPE_SEARCH_MARKET_DIVIDER
     77             | VIEW_TYPE_PREDICTION_DIVIDER;
     78     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
     79             | VIEW_TYPE_PREDICTION_ICON;
     80     public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
     81             | VIEW_TYPE_DISCOVERY_ITEM;
     82 
     83 
     84     public interface BindViewCallback {
     85         void onBindView(ViewHolder holder);
     86     }
     87 
     88     /**
     89      * ViewHolder for each icon.
     90      */
     91     public static class ViewHolder extends RecyclerView.ViewHolder {
     92         public ViewHolder(View v) {
     93             super(v);
     94         }
     95     }
     96 
     97     /**
     98      * A subclass of GridLayoutManager that overrides accessibility values during app search.
     99      */
    100     public class AppsGridLayoutManager extends GridLayoutManager {
    101 
    102         public AppsGridLayoutManager(Context context) {
    103             super(context, 1, GridLayoutManager.VERTICAL, false);
    104         }
    105 
    106         @Override
    107         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    108             super.onInitializeAccessibilityEvent(event);
    109 
    110             // Ensure that we only report the number apps for accessibility not including other
    111             // adapter views
    112             final AccessibilityRecordCompat record = AccessibilityEventCompat
    113                     .asRecord(event);
    114             record.setItemCount(mApps.getNumFilteredApps());
    115             record.setFromIndex(Math.max(0,
    116                     record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
    117             record.setToIndex(Math.max(0,
    118                     record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
    119         }
    120 
    121         @Override
    122         public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
    123                 RecyclerView.State state) {
    124             return super.getRowCountForAccessibility(recycler, state) -
    125                     getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
    126         }
    127 
    128         @Override
    129         public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
    130                 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
    131             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
    132 
    133             ViewGroup.LayoutParams lp = host.getLayoutParams();
    134             AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
    135             if (!(lp instanceof LayoutParams) || (cic == null)) {
    136                 return;
    137             }
    138             LayoutParams glp = (LayoutParams) lp;
    139             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
    140                     cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
    141                     cic.getRowSpan(),
    142                     cic.getColumnIndex(),
    143                     cic.getColumnSpan(),
    144                     cic.isHeading(),
    145                     cic.isSelected()));
    146         }
    147 
    148         /**
    149          * Returns the number of rows before {@param adapterPosition}, including this position
    150          * which should not be counted towards the collection info.
    151          */
    152         private int getRowsNotForAccessibility(int adapterPosition) {
    153             List<AdapterItem> items = mApps.getAdapterItems();
    154             adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
    155             int extraRows = 0;
    156             for (int i = 0; i <= adapterPosition; i++) {
    157                 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
    158                     extraRows++;
    159                 }
    160             }
    161             return extraRows;
    162         }
    163 
    164         @Override
    165         public int getPaddingBottom() {
    166             return mLauncher.getDragLayer().getInsets().bottom;
    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 AllAppsSearchBarController mSearchController;
    203     private OnFocusChangeListener mIconFocusListener;
    204 
    205     // The text to show when there are no search results and no market search handler.
    206     private String mEmptySearchMessage;
    207     // The intent to send off to the market app, updated each time the search query changes.
    208     private Intent mMarketSearchIntent;
    209 
    210     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
    211             iconClickListener, View.OnLongClickListener iconLongClickListener) {
    212         Resources res = launcher.getResources();
    213         mLauncher = launcher;
    214         mApps = apps;
    215         mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
    216         mGridSizer = new GridSpanSizer();
    217         mGridLayoutMgr = new AppsGridLayoutManager(launcher);
    218         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
    219         mLayoutInflater = LayoutInflater.from(launcher);
    220         mIconClickListener = iconClickListener;
    221         mIconLongClickListener = iconLongClickListener;
    222     }
    223 
    224     public static boolean isDividerViewType(int viewType) {
    225         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
    226     }
    227 
    228     public static boolean isIconViewType(int viewType) {
    229         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
    230     }
    231 
    232     public static boolean isViewType(int viewType, int viewTypeMask) {
    233         return (viewType & viewTypeMask) != 0;
    234     }
    235 
    236     /**
    237      * Sets the number of apps per row.
    238      */
    239     public void setNumAppsPerRow(int appsPerRow) {
    240         mAppsPerRow = appsPerRow;
    241         mGridLayoutMgr.setSpanCount(appsPerRow);
    242     }
    243 
    244     public void setSearchController(AllAppsSearchBarController searchController) {
    245         mSearchController = searchController;
    246     }
    247 
    248     public void setIconFocusListener(OnFocusChangeListener focusListener) {
    249         mIconFocusListener = focusListener;
    250     }
    251 
    252     /**
    253      * Sets the last search query that was made, used to show when there are no results and to also
    254      * seed the intent for searching the market.
    255      */
    256     public void setLastSearchQuery(String query) {
    257         Resources res = mLauncher.getResources();
    258         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
    259         mMarketSearchIntent = mSearchController.createMarketSearchIntent(query);
    260     }
    261 
    262     /**
    263      * Sets the callback for when views are bound.
    264      */
    265     public void setBindViewCallback(BindViewCallback cb) {
    266         mBindViewCallback = cb;
    267     }
    268 
    269     /**
    270      * Returns the grid layout manager.
    271      */
    272     public GridLayoutManager getLayoutManager() {
    273         return mGridLayoutMgr;
    274     }
    275 
    276     @Override
    277     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    278         switch (viewType) {
    279             case VIEW_TYPE_ICON:
    280             case VIEW_TYPE_PREDICTION_ICON:
    281                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
    282                         R.layout.all_apps_icon, parent, false);
    283                 icon.setOnClickListener(mIconClickListener);
    284                 icon.setOnLongClickListener(mIconLongClickListener);
    285                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
    286                         .getLongPressTimeout());
    287                 icon.setOnFocusChangeListener(mIconFocusListener);
    288 
    289                 // Ensure the all apps icon height matches the workspace icons
    290                 icon.getLayoutParams().height = getCellSize().y;
    291                 return new ViewHolder(icon);
    292             case VIEW_TYPE_DISCOVERY_ITEM:
    293                 AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
    294                         .inflate(R.layout.all_apps_discovery_item, parent, false);
    295                 appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
    296                         mIconLongClickListener);
    297                 return new ViewHolder(appDiscoveryItemView);
    298             case VIEW_TYPE_EMPTY_SEARCH:
    299                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
    300                         parent, false));
    301             case VIEW_TYPE_SEARCH_MARKET:
    302                 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
    303                         parent, false);
    304                 searchMarketView.setOnClickListener(new View.OnClickListener() {
    305                     @Override
    306                     public void onClick(View v) {
    307                         mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
    308                     }
    309                 });
    310                 return new ViewHolder(searchMarketView);
    311             case VIEW_TYPE_SEARCH_DIVIDER:
    312                 return new ViewHolder(mLayoutInflater.inflate(
    313                         R.layout.all_apps_search_divider, parent, false));
    314             case VIEW_TYPE_APPS_LOADING_DIVIDER:
    315                 View loadingDividerView = mLayoutInflater.inflate(
    316                         R.layout.all_apps_discovery_loading_divider, parent, false);
    317                 return new ViewHolder(loadingDividerView);
    318             case VIEW_TYPE_PREDICTION_DIVIDER:
    319             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
    320                 return new ViewHolder(mLayoutInflater.inflate(
    321                         R.layout.all_apps_divider, parent, false));
    322             default:
    323                 throw new RuntimeException("Unexpected view type");
    324         }
    325     }
    326 
    327     private Point getCellSize() {
    328         return mLauncher.getDeviceProfile().getCellSize();
    329     }
    330 
    331     @Override
    332     public void onBindViewHolder(ViewHolder holder, int position) {
    333         switch (holder.getItemViewType()) {
    334             case VIEW_TYPE_ICON:
    335             case VIEW_TYPE_PREDICTION_ICON:
    336                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
    337                 BubbleTextView icon = (BubbleTextView) holder.itemView;
    338                 icon.applyFromApplicationInfo(info);
    339                 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
    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 boolean onFailedToRecycleView(ViewHolder holder) {
    378         // Always recycle and we will reset the view when it is bound
    379         return true;
    380     }
    381 
    382     @Override
    383     public int getItemCount() {
    384         return mApps.getAdapterItems().size();
    385     }
    386 
    387     @Override
    388     public int getItemViewType(int position) {
    389         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
    390         return item.viewType;
    391     }
    392 }
    393