Home | History | Annotate | Download | only in dashboard
      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.settings.dashboard;
     17 
     18 import android.app.Activity;
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Color;
     23 import android.graphics.drawable.Drawable;
     24 import android.graphics.drawable.Icon;
     25 import android.os.Bundle;
     26 import android.support.annotation.VisibleForTesting;
     27 import android.support.v7.util.DiffUtil;
     28 import android.support.v7.widget.LinearLayoutManager;
     29 import android.support.v7.widget.RecyclerView;
     30 import android.text.TextUtils;
     31 import android.util.ArrayMap;
     32 import android.util.Log;
     33 import android.util.Pair;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.ImageView;
     38 import android.widget.LinearLayout;
     39 import android.widget.TextView;
     40 
     41 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     42 import com.android.settings.R;
     43 import com.android.settings.R.id;
     44 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
     45 import com.android.settings.dashboard.DashboardData.SuggestionConditionHeaderData;
     46 import com.android.settings.dashboard.conditional.Condition;
     47 import com.android.settings.dashboard.conditional.ConditionAdapter;
     48 import com.android.settings.dashboard.suggestions.SuggestionAdapter;
     49 import com.android.settings.dashboard.suggestions.SuggestionDismissController;
     50 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
     51 import com.android.settings.dashboard.suggestions.SuggestionLogHelper;
     52 import com.android.settings.overlay.FeatureFactory;
     53 import com.android.settingslib.Utils;
     54 import com.android.settingslib.drawer.DashboardCategory;
     55 import com.android.settingslib.drawer.Tile;
     56 import com.android.settingslib.suggestions.SuggestionParser;
     57 
     58 import java.util.ArrayList;
     59 import java.util.List;
     60 
     61 public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
     62         implements SummaryLoader.SummaryConsumer {
     63     public static final String TAG = "DashboardAdapter";
     64     private static final String STATE_SUGGESTION_LIST = "suggestion_list";
     65     private static final String STATE_CATEGORY_LIST = "category_list";
     66     private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged";
     67 
     68     @VisibleForTesting
     69     static final String STATE_SUGGESTION_CONDITION_MODE = "suggestion_condition_mode";
     70     @VisibleForTesting
     71     static final int SUGGESTION_CONDITION_HEADER_POSITION = 0;
     72     @VisibleForTesting
     73     static final int MAX_SUGGESTION_TO_SHOW = 5;
     74 
     75     private final IconCache mCache;
     76     private final Context mContext;
     77     private final MetricsFeatureProvider mMetricsFeatureProvider;
     78     private final DashboardFeatureProvider mDashboardFeatureProvider;
     79     private final SuggestionFeatureProvider mSuggestionFeatureProvider;
     80     private final ArrayList<String> mSuggestionsShownLogged;
     81     private boolean mFirstFrameDrawn;
     82     private RecyclerView mRecyclerView;
     83     private SuggestionParser mSuggestionParser;
     84     private SuggestionAdapter mSuggestionAdapter;
     85     private SuggestionDismissController mSuggestionDismissHandler;
     86     private SuggestionDismissController.Callback mCallback;
     87 
     88     @VisibleForTesting
     89     DashboardData mDashboardData;
     90 
     91     private View.OnClickListener mTileClickListener = new View.OnClickListener() {
     92         @Override
     93         public void onClick(View v) {
     94             //TODO: get rid of setTag/getTag
     95             mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag());
     96         }
     97     };
     98 
     99     private View.OnClickListener mConditionClickListener = new View.OnClickListener() {
    100 
    101         @Override
    102         public void onClick(View v) {
    103             Condition condition = (Condition) v.getTag();
    104             //TODO: get rid of setTag/getTag
    105             mMetricsFeatureProvider.action(mContext,
    106                     MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
    107                     condition.getMetricsConstant());
    108             condition.onPrimaryClick();
    109         }
    110     };
    111 
    112     public DashboardAdapter(Context context, Bundle savedInstanceState,
    113             List<Condition> conditions, SuggestionParser suggestionParser,
    114             SuggestionDismissController.Callback callback) {
    115         List<Tile> suggestions = null;
    116         DashboardCategory category = null;
    117         int suggestionConditionMode = DashboardData.HEADER_MODE_DEFAULT;
    118 
    119         mContext = context;
    120         final FeatureFactory factory = FeatureFactory.getFactory(context);
    121         mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
    122         mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
    123         mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context);
    124         mCache = new IconCache(context);
    125         mSuggestionParser = suggestionParser;
    126         mCallback = callback;
    127 
    128         setHasStableIds(true);
    129 
    130         if (savedInstanceState != null) {
    131             suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
    132             category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST);
    133             suggestionConditionMode = savedInstanceState.getInt(
    134                     STATE_SUGGESTION_CONDITION_MODE, suggestionConditionMode);
    135             mSuggestionsShownLogged = savedInstanceState.getStringArrayList(
    136                     STATE_SUGGESTIONS_SHOWN_LOGGED);
    137         } else {
    138             mSuggestionsShownLogged = new ArrayList<>();
    139         }
    140 
    141         mDashboardData = new DashboardData.Builder()
    142                 .setConditions(conditions)
    143                 .setSuggestions(suggestions)
    144                 .setCategory(category)
    145                 .setSuggestionConditionMode(suggestionConditionMode)
    146                 .build();
    147     }
    148 
    149     public List<Tile> getSuggestions() {
    150         return mDashboardData.getSuggestions();
    151     }
    152 
    153     public void setCategoriesAndSuggestions(DashboardCategory category,
    154             List<Tile> suggestions) {
    155         tintIcons(category, suggestions);
    156 
    157         final DashboardData prevData = mDashboardData;
    158         mDashboardData = new DashboardData.Builder(prevData)
    159                 .setSuggestions(suggestions.subList(0,
    160                         Math.min(suggestions.size(), MAX_SUGGESTION_TO_SHOW)))
    161                 .setCategory(category)
    162                 .build();
    163         notifyDashboardDataChanged(prevData);
    164         List<Tile> shownSuggestions = null;
    165         final int mode = mDashboardData.getSuggestionConditionMode();
    166         if (mode == DashboardData.HEADER_MODE_DEFAULT) {
    167             shownSuggestions = suggestions.subList(0,
    168                     Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT));
    169         } else if (mode != DashboardData.HEADER_MODE_COLLAPSED) {
    170             shownSuggestions = suggestions;
    171         }
    172         if (shownSuggestions != null) {
    173             for (Tile suggestion : shownSuggestions) {
    174                 final String identifier = mSuggestionFeatureProvider.getSuggestionIdentifier(
    175                         mContext, suggestion);
    176                 mMetricsFeatureProvider.action(
    177                         mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, identifier,
    178                         getSuggestionTaggedData());
    179                 mSuggestionsShownLogged.add(identifier);
    180             }
    181         }
    182     }
    183 
    184     public void setCategory(DashboardCategory category) {
    185         tintIcons(category, null);
    186         final DashboardData prevData = mDashboardData;
    187         Log.d(TAG, "adapter setCategory called");
    188         mDashboardData = new DashboardData.Builder(prevData)
    189                 .setCategory(category)
    190                 .build();
    191         notifyDashboardDataChanged(prevData);
    192     }
    193 
    194     public void setConditions(List<Condition> conditions) {
    195         final DashboardData prevData = mDashboardData;
    196         Log.d(TAG, "adapter setConditions called");
    197         mDashboardData = new DashboardData.Builder(prevData)
    198                 .setConditions(conditions)
    199                 .build();
    200         notifyDashboardDataChanged(prevData);
    201     }
    202 
    203     public void onSuggestionDismissed(Tile suggestion) {
    204         final List<Tile> suggestions = mDashboardData.getSuggestions();
    205         if (suggestions == null || suggestions.isEmpty()) {
    206             return;
    207         }
    208         if (suggestions.size() == 1) {
    209             // The only suggestion is dismissed, and the the empty suggestion container will
    210             // remain as the dashboard item. Need to refresh the dashboard list.
    211             final DashboardData prevData = mDashboardData;
    212             mDashboardData = new DashboardData.Builder(prevData)
    213                     .setSuggestions(null)
    214                     .build();
    215             notifyDashboardDataChanged(prevData);
    216         } else {
    217             mSuggestionAdapter.removeSuggestion(suggestion);
    218         }
    219     }
    220 
    221     @Override
    222     public void notifySummaryChanged(Tile tile) {
    223         final int position = mDashboardData.getPositionByTile(tile);
    224         if (position != DashboardData.POSITION_NOT_FOUND) {
    225             // Since usually tile in parameter and tile in mCategories are same instance,
    226             // which is hard to be detected by DiffUtil, so we notifyItemChanged directly.
    227             notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position));
    228         }
    229     }
    230 
    231     @Override
    232     public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    233         final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
    234         if (viewType == R.layout.suggestion_condition_header) {
    235             return new SuggestionAndConditionHeaderHolder(view);
    236         }
    237         if (viewType == R.layout.suggestion_condition_container) {
    238             return new SuggestionAndConditionContainerHolder(view);
    239         }
    240         return new DashboardItemHolder(view);
    241     }
    242 
    243     @Override
    244     public void onBindViewHolder(DashboardItemHolder holder, int position) {
    245         final int type = mDashboardData.getItemTypeByPosition(position);
    246         switch (type) {
    247             case R.layout.dashboard_tile:
    248                 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position);
    249                 onBindTile(holder, tile);
    250                 holder.itemView.setTag(tile);
    251                 holder.itemView.setOnClickListener(mTileClickListener);
    252                 break;
    253             case R.layout.suggestion_condition_container:
    254                 onBindConditionAndSuggestion(
    255                         (SuggestionAndConditionContainerHolder) holder, position);
    256                 break;
    257             case R.layout.suggestion_condition_header:
    258                 onBindSuggestionConditionHeader((SuggestionAndConditionHeaderHolder) holder,
    259                         (SuggestionConditionHeaderData)
    260                                 mDashboardData.getItemEntityByPosition(position));
    261                 break;
    262             case R.layout.suggestion_condition_footer:
    263                 holder.itemView.setOnClickListener(v -> {
    264                     mMetricsFeatureProvider.action(mContext,
    265                             MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false);
    266                     DashboardData prevData = mDashboardData;
    267                     mDashboardData = new DashboardData.Builder(prevData).setSuggestionConditionMode(
    268                             DashboardData.HEADER_MODE_COLLAPSED).build();
    269                     notifyDashboardDataChanged(prevData);
    270                     mRecyclerView.scrollToPosition(SUGGESTION_CONDITION_HEADER_POSITION);
    271                 });
    272                 break;
    273         }
    274     }
    275 
    276     @Override
    277     public long getItemId(int position) {
    278         return mDashboardData.getItemIdByPosition(position);
    279     }
    280 
    281     @Override
    282     public int getItemViewType(int position) {
    283         return mDashboardData.getItemTypeByPosition(position);
    284     }
    285 
    286     @Override
    287     public int getItemCount() {
    288         return mDashboardData.size();
    289     }
    290 
    291     @Override
    292     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    293         super.onAttachedToRecyclerView(recyclerView);
    294         // save the view so that we can scroll it when expanding/collapsing the suggestion and
    295         // conditions.
    296         mRecyclerView = recyclerView;
    297     }
    298 
    299     public void onPause() {
    300         if (mDashboardData.getSuggestions() == null) {
    301             return;
    302         }
    303         for (Tile suggestion : mDashboardData.getSuggestions()) {
    304             String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
    305                     mContext, suggestion);
    306             if (mSuggestionsShownLogged.contains(suggestionId)) {
    307                 mMetricsFeatureProvider.action(
    308                         mContext, MetricsEvent.ACTION_HIDE_SETTINGS_SUGGESTION, suggestionId,
    309                         getSuggestionTaggedData());
    310             }
    311         }
    312         mSuggestionsShownLogged.clear();
    313     }
    314 
    315     public Object getItem(long itemId) {
    316         return mDashboardData.getItemEntityById(itemId);
    317     }
    318 
    319     public Tile getSuggestion(int position) {
    320         return mSuggestionAdapter.getSuggestion(position);
    321     }
    322 
    323     @VisibleForTesting
    324     void notifyDashboardDataChanged(DashboardData prevData) {
    325         if (mFirstFrameDrawn && prevData != null) {
    326             final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData
    327                     .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList()));
    328             diffResult.dispatchUpdatesTo(this);
    329         } else {
    330             mFirstFrameDrawn = true;
    331             notifyDataSetChanged();
    332         }
    333     }
    334 
    335     private void logSuggestions() {
    336         final List<Tile> suggestions = mDashboardData.getSuggestions();
    337         if (suggestions == null) {
    338             return;
    339         }
    340         for (Tile suggestion : suggestions) {
    341             final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
    342                     mContext, suggestion);
    343             if (!mSuggestionsShownLogged.contains(suggestionId)) {
    344                 mMetricsFeatureProvider.action(
    345                         mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, suggestionId,
    346                         getSuggestionTaggedData());
    347                 mSuggestionsShownLogged.add(suggestionId);
    348             }
    349         }
    350     }
    351 
    352     @VisibleForTesting
    353     void onBindSuggestionConditionHeader(final SuggestionAndConditionHeaderHolder holder,
    354             SuggestionConditionHeaderData data) {
    355         final int curMode = mDashboardData.getSuggestionConditionMode();
    356         final int nextMode = data.hiddenSuggestionCount > 0 && data.conditionCount > 0
    357                 && curMode != DashboardData.HEADER_MODE_SUGGESTION_EXPANDED
    358                 ? DashboardData.HEADER_MODE_SUGGESTION_EXPANDED
    359                 : DashboardData.HEADER_MODE_FULLY_EXPANDED;
    360         final boolean moreSuggestions = data.hiddenSuggestionCount > 0;
    361         final boolean hasConditions = data.conditionCount > 0;
    362         if (data.conditionCount > 0) {
    363             holder.icon.setImageIcon(data.conditionIcons.get(0));
    364             holder.icon.setVisibility(View.VISIBLE);
    365             if (data.conditionCount == 1) {
    366                 holder.title.setText(data.title);
    367                 holder.title.setTextColor(Utils.getColorAccent(mContext));
    368                 holder.icons.setVisibility(View.INVISIBLE);
    369             } else {
    370                 holder.title.setText(null);
    371                 updateConditionIcons(data.conditionIcons, holder.icons);
    372                 holder.icons.setVisibility(View.VISIBLE);
    373             }
    374         } else {
    375             holder.icon.setVisibility(View.INVISIBLE);
    376             holder.icons.setVisibility(View.INVISIBLE);
    377         }
    378 
    379         if (data.hiddenSuggestionCount > 0) {
    380             holder.summary.setTextColor(Color.BLACK);
    381             if (curMode == DashboardData.HEADER_MODE_COLLAPSED) {
    382                 if (data.conditionCount > 0) {
    383                     holder.summary.setText(mContext.getResources().getQuantityString(
    384                             R.plurals.suggestions_collapsed_summary,
    385                             data.hiddenSuggestionCount, data.hiddenSuggestionCount));
    386                 } else {
    387                     holder.title.setText(mContext.getResources().getQuantityString(
    388                             R.plurals.suggestions_collapsed_title,
    389                             data.hiddenSuggestionCount, data.hiddenSuggestionCount));
    390                     holder.title.setTextColor(Color.BLACK);
    391                     holder.summary.setText(null);
    392                 }
    393             } else if (curMode == DashboardData.HEADER_MODE_DEFAULT) {
    394                 if (data.conditionCount > 0) {
    395                     holder.summary.setText(mContext.getString(
    396                             R.string.suggestions_summary, data.hiddenSuggestionCount));
    397                 } else {
    398                     holder.title.setText(mContext.getString(
    399                             R.string.suggestions_more_title, data.hiddenSuggestionCount));
    400                     holder.title.setTextColor(Color.BLACK);
    401                     holder.summary.setText(null);
    402                 }
    403             }
    404         } else if (data.conditionCount > 1) {
    405             holder.summary.setTextColor(Utils.getColorAccent(mContext));
    406             holder.summary.setText(
    407                     mContext.getString(R.string.condition_summary, data.conditionCount));
    408         } else {
    409             holder.summary.setText(null);
    410         }
    411 
    412         final Resources res = mContext.getResources();
    413         final int padding = res.getDimensionPixelOffset(
    414                 curMode == DashboardData.HEADER_MODE_COLLAPSED
    415                         ? R.dimen.suggestion_condition_header_padding_collapsed
    416                         : R.dimen.suggestion_condition_header_padding_expanded);
    417         holder.itemView.setPadding(0, padding, 0, padding);
    418 
    419         holder.itemView.setOnClickListener(v -> {
    420             if (moreSuggestions) {
    421                 logSuggestions();
    422             } else if (hasConditions) {
    423                 mMetricsFeatureProvider.action(mContext,
    424                         MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true);
    425             }
    426             DashboardData prevData = mDashboardData;
    427             final boolean wasCollapsed = curMode == DashboardData.HEADER_MODE_COLLAPSED;
    428             mDashboardData = new DashboardData.Builder(prevData)
    429                     .setSuggestionConditionMode(nextMode).build();
    430             notifyDashboardDataChanged(prevData);
    431             if (wasCollapsed) {
    432                 mRecyclerView.scrollToPosition(SUGGESTION_CONDITION_HEADER_POSITION);
    433             }
    434         });
    435     }
    436 
    437     @VisibleForTesting
    438     void onBindConditionAndSuggestion(final SuggestionAndConditionContainerHolder holder,
    439             int position) {
    440         // If there is suggestions to show, it will be at position 0 as we don't show the suggestion
    441         // header anymore.
    442         final List<Tile> suggestions = mDashboardData.getSuggestions();
    443         if (position == SUGGESTION_CONDITION_HEADER_POSITION
    444                 && suggestions != null && suggestions.size() > 0) {
    445             mSuggestionAdapter = new SuggestionAdapter(mContext, (List<Tile>)
    446                     mDashboardData.getItemEntityByPosition(position), mSuggestionsShownLogged);
    447             mSuggestionDismissHandler = new SuggestionDismissController(mContext,
    448                     holder.data, mSuggestionParser, mCallback);
    449             holder.data.setAdapter(mSuggestionAdapter);
    450         } else {
    451             ConditionAdapter adapter = new ConditionAdapter(mContext,
    452                     (List<Condition>) mDashboardData.getItemEntityByPosition(position),
    453                     mDashboardData.getSuggestionConditionMode());
    454             adapter.addDismissHandling(holder.data);
    455             holder.data.setAdapter(adapter);
    456         }
    457         holder.data.setLayoutManager(new LinearLayoutManager(mContext));
    458     }
    459 
    460     private void onBindTile(DashboardItemHolder holder, Tile tile) {
    461         if (tile.remoteViews != null) {
    462             final ViewGroup itemView = (ViewGroup) holder.itemView;
    463             itemView.removeAllViews();
    464             itemView.addView(tile.remoteViews.apply(itemView.getContext(), itemView));
    465         } else {
    466             holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
    467             holder.title.setText(tile.title);
    468             if (!TextUtils.isEmpty(tile.summary)) {
    469                 holder.summary.setText(tile.summary);
    470                 holder.summary.setVisibility(View.VISIBLE);
    471             } else {
    472                 holder.summary.setVisibility(View.GONE);
    473             }
    474         }
    475     }
    476 
    477     private void tintIcons(DashboardCategory category, List<Tile> suggestions) {
    478         if (!mDashboardFeatureProvider.shouldTintIcon()) {
    479             return;
    480         }
    481         // TODO: Better place for tinting?
    482         final TypedArray a = mContext.obtainStyledAttributes(new int[]{
    483                 android.R.attr.colorControlNormal});
    484         final int tintColor = a.getColor(0, mContext.getColor(R.color.fallback_tintColor));
    485         a.recycle();
    486         if (category != null) {
    487             for (Tile tile : category.tiles) {
    488                 if (tile.isIconTintable) {
    489                     // If this drawable is tintable, tint it to match the color.
    490                     tile.icon.setTint(tintColor);
    491                 }
    492             }
    493         }
    494         if (suggestions != null) {
    495             for (Tile suggestion : suggestions) {
    496                 if (suggestion.isIconTintable) {
    497                     suggestion.icon.setTint(tintColor);
    498                 }
    499             }
    500         }
    501     }
    502 
    503     void onSaveInstanceState(Bundle outState) {
    504         final List<Tile> suggestions = mDashboardData.getSuggestions();
    505         final DashboardCategory category = mDashboardData.getCategory();
    506         if (suggestions != null) {
    507             outState.putParcelableArrayList(STATE_SUGGESTION_LIST, new ArrayList<>(suggestions));
    508         }
    509         if (category != null) {
    510             outState.putParcelable(STATE_CATEGORY_LIST, category);
    511         }
    512         outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged);
    513         outState.putInt(STATE_SUGGESTION_CONDITION_MODE,
    514                 mDashboardData.getSuggestionConditionMode());
    515     }
    516 
    517     private void updateConditionIcons(List<Icon> icons, ViewGroup parent) {
    518         if (icons == null || icons.size() < 2) {
    519             parent.setVisibility(View.INVISIBLE);
    520             return;
    521         }
    522         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    523         parent.removeAllViews();
    524         for (int i = 1, size = icons.size(); i < size; i++) {
    525             ImageView icon = (ImageView) inflater.inflate(
    526                     R.layout.condition_header_icon, parent, false);
    527             icon.setImageIcon(icons.get(i));
    528             parent.addView(icon);
    529         }
    530         parent.setVisibility(View.VISIBLE);
    531     }
    532 
    533     private Pair<Integer, Object>[] getSuggestionTaggedData() {
    534         return SuggestionLogHelper.getSuggestionTaggedData(
    535                 mSuggestionFeatureProvider.isSmartSuggestionEnabled(mContext));
    536     }
    537 
    538     public static class IconCache {
    539         private final Context mContext;
    540         private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();
    541 
    542         public IconCache(Context context) {
    543             mContext = context;
    544         }
    545 
    546         public Drawable getIcon(Icon icon) {
    547             Drawable drawable = mMap.get(icon);
    548             if (drawable == null) {
    549                 drawable = icon.loadDrawable(mContext);
    550                 mMap.put(icon, drawable);
    551             }
    552             return drawable;
    553         }
    554     }
    555 
    556     public static class DashboardItemHolder extends RecyclerView.ViewHolder {
    557         public final ImageView icon;
    558         public final TextView title;
    559         public final TextView summary;
    560 
    561         public DashboardItemHolder(View itemView) {
    562             super(itemView);
    563             icon = itemView.findViewById(android.R.id.icon);
    564             title = itemView.findViewById(android.R.id.title);
    565             summary = itemView.findViewById(android.R.id.summary);
    566         }
    567     }
    568 
    569     public static class SuggestionAndConditionHeaderHolder extends DashboardItemHolder {
    570         public final LinearLayout icons;
    571         public final ImageView expandIndicator;
    572 
    573         public SuggestionAndConditionHeaderHolder(View itemView) {
    574             super(itemView);
    575             icons = itemView.findViewById(id.additional_icons);
    576             expandIndicator = itemView.findViewById(id.expand_indicator);
    577         }
    578     }
    579 
    580     public static class SuggestionAndConditionContainerHolder extends DashboardItemHolder {
    581         public final RecyclerView data;
    582 
    583         public SuggestionAndConditionContainerHolder(View itemView) {
    584             super(itemView);
    585             data = itemView.findViewById(id.data);
    586         }
    587     }
    588 
    589 }
    590