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.TypedArray;
     21 import android.graphics.drawable.Drawable;
     22 import android.graphics.drawable.Icon;
     23 import android.os.Bundle;
     24 import android.support.annotation.VisibleForTesting;
     25 import android.support.v7.util.DiffUtil;
     26 import android.support.v7.widget.RecyclerView;
     27 import android.text.TextUtils;
     28 import android.util.ArrayMap;
     29 import android.util.Log;
     30 import android.view.LayoutInflater;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import android.widget.ImageView;
     34 import android.widget.TextView;
     35 
     36 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     37 import com.android.settings.R;
     38 import com.android.settings.SettingsActivity;
     39 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
     40 import com.android.settings.dashboard.conditional.Condition;
     41 import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
     42 import com.android.settings.dashboard.suggestions.SuggestionDismissController;
     43 import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
     44 import com.android.settings.overlay.FeatureFactory;
     45 import com.android.settingslib.drawer.DashboardCategory;
     46 import com.android.settingslib.drawer.Tile;
     47 
     48 import java.util.ArrayList;
     49 import java.util.List;
     50 
     51 public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
     52         implements SummaryLoader.SummaryConsumer, SuggestionDismissController.Callback {
     53     public static final String TAG = "DashboardAdapter";
     54     private static final String STATE_SUGGESTION_LIST = "suggestion_list";
     55     private static final String STATE_CATEGORY_LIST = "category_list";
     56     private static final String STATE_SUGGESTION_MODE = "suggestion_mode";
     57     private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged";
     58 
     59     private final IconCache mCache;
     60     private final Context mContext;
     61     private final MetricsFeatureProvider mMetricsFeatureProvider;
     62     private final DashboardFeatureProvider mDashboardFeatureProvider;
     63     private final SuggestionFeatureProvider mSuggestionFeatureProvider;
     64     private final ArrayList<String> mSuggestionsShownLogged;
     65     private boolean mFirstFrameDrawn;
     66 
     67     @VisibleForTesting
     68     DashboardData mDashboardData;
     69 
     70     private View.OnClickListener mTileClickListener = new View.OnClickListener() {
     71         @Override
     72         public void onClick(View v) {
     73             //TODO: get rid of setTag/getTag
     74             mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag());
     75         }
     76     };
     77 
     78     private View.OnClickListener mConditionClickListener = new View.OnClickListener() {
     79 
     80         @Override
     81         public void onClick(View v) {
     82             Condition expandedCondition = mDashboardData.getExpandedCondition();
     83 
     84             //TODO: get rid of setTag/getTag
     85             if (v.getTag() == expandedCondition) {
     86                 mMetricsFeatureProvider.action(mContext,
     87                         MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
     88                         expandedCondition.getMetricsConstant());
     89                 expandedCondition.onPrimaryClick();
     90             } else {
     91                 expandedCondition = (Condition) v.getTag();
     92                 mMetricsFeatureProvider.action(mContext,
     93                         MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
     94                         expandedCondition.getMetricsConstant());
     95 
     96                 updateExpandedCondition(expandedCondition);
     97             }
     98         }
     99     };
    100 
    101     public DashboardAdapter(Context context, Bundle savedInstanceState,
    102             List<Condition> conditions) {
    103         List<Tile> suggestions = null;
    104         List<DashboardCategory> categories = null;
    105         int suggestionMode = DashboardData.SUGGESTION_MODE_DEFAULT;
    106 
    107         mContext = context;
    108         final FeatureFactory factory = FeatureFactory.getFactory(context);
    109         mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
    110         mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
    111         mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context);
    112         mCache = new IconCache(context);
    113 
    114         setHasStableIds(true);
    115 
    116         if (savedInstanceState != null) {
    117             suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
    118             categories = savedInstanceState.getParcelableArrayList(STATE_CATEGORY_LIST);
    119             suggestionMode = savedInstanceState.getInt(
    120                     STATE_SUGGESTION_MODE, DashboardData.SUGGESTION_MODE_DEFAULT);
    121             mSuggestionsShownLogged = savedInstanceState.getStringArrayList(
    122                     STATE_SUGGESTIONS_SHOWN_LOGGED);
    123         } else {
    124             mSuggestionsShownLogged = new ArrayList<>();
    125         }
    126 
    127         mDashboardData = new DashboardData.Builder()
    128                 .setConditions(conditions)
    129                 .setSuggestions(suggestions)
    130                 .setCategories(categories)
    131                 .setSuggestionMode(suggestionMode)
    132                 .build();
    133     }
    134 
    135     public List<Tile> getSuggestions() {
    136         return mDashboardData.getSuggestions();
    137     }
    138 
    139     public void setCategoriesAndSuggestions(List<DashboardCategory> categories,
    140             List<Tile> suggestions) {
    141         // TODO: Better place for tinting?
    142         final TypedArray a = mContext.obtainStyledAttributes(new int[]{
    143                 android.R.attr.colorControlNormal});
    144         int tintColor = a.getColor(0, mContext.getColor(android.R.color.white));
    145         a.recycle();
    146         for (int i = 0; i < categories.size(); i++) {
    147             for (int j = 0; j < categories.get(i).tiles.size(); j++) {
    148                 final Tile tile = categories.get(i).tiles.get(j);
    149 
    150                 if (!mContext.getPackageName().equals(
    151                         tile.intent.getComponent().getPackageName())) {
    152                     // If this drawable is coming from outside Settings, tint it to match the
    153                     // color.
    154                     tile.icon.setTint(tintColor);
    155                 }
    156             }
    157         }
    158 
    159         final DashboardData prevData = mDashboardData;
    160         mDashboardData = new DashboardData.Builder(prevData)
    161                 .setSuggestions(suggestions)
    162                 .setCategories(categories)
    163                 .build();
    164         notifyDashboardDataChanged(prevData);
    165         List<Tile> shownSuggestions = null;
    166         switch (mDashboardData.getSuggestionMode()) {
    167             case DashboardData.SUGGESTION_MODE_DEFAULT:
    168                 shownSuggestions = suggestions.subList(0,
    169                         Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT));
    170                 break;
    171             case DashboardData.SUGGESTION_MODE_EXPANDED:
    172                 shownSuggestions = suggestions;
    173                 break;
    174         }
    175         if (shownSuggestions != null) {
    176             for (Tile suggestion : shownSuggestions) {
    177                 final String identifier = mSuggestionFeatureProvider.getSuggestionIdentifier(
    178                         mContext, suggestion);
    179                 mMetricsFeatureProvider.action(
    180                         mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, identifier);
    181                 mSuggestionsShownLogged.add(identifier);
    182             }
    183         }
    184     }
    185 
    186     public void setCategory(List<DashboardCategory> category) {
    187         final DashboardData prevData = mDashboardData;
    188         Log.d(TAG, "adapter setCategory called");
    189         mDashboardData = new DashboardData.Builder(prevData)
    190                 .setCategories(category)
    191                 .build();
    192         notifyDashboardDataChanged(prevData);
    193     }
    194 
    195     public void setConditions(List<Condition> conditions) {
    196         final DashboardData prevData = mDashboardData;
    197         Log.d(TAG, "adapter setConditions called");
    198         mDashboardData = new DashboardData.Builder(prevData)
    199                 .setConditions(conditions)
    200                 .setExpandedCondition(null)
    201                 .build();
    202         notifyDashboardDataChanged(prevData);
    203     }
    204 
    205     @Override
    206     public void notifySummaryChanged(Tile tile) {
    207         final int position = mDashboardData.getPositionByTile(tile);
    208         if (position != DashboardData.POSITION_NOT_FOUND) {
    209             // Since usually tile in parameter and tile in mCategories are same instance,
    210             // which is hard to be detected by DiffUtil, so we notifyItemChanged directly.
    211             notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position));
    212         }
    213     }
    214 
    215     @Override
    216     public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    217         return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
    218                 viewType, parent, false));
    219     }
    220 
    221     @Override
    222     public void onBindViewHolder(DashboardItemHolder holder, int position) {
    223         final int type = mDashboardData.getItemTypeByPosition(position);
    224         switch (type) {
    225             case R.layout.dashboard_category:
    226                 onBindCategory(holder,
    227                         (DashboardCategory) mDashboardData.getItemEntityByPosition(position));
    228                 break;
    229             case R.layout.dashboard_tile:
    230                 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position);
    231                 onBindTile(holder, tile);
    232                 holder.itemView.setTag(tile);
    233                 holder.itemView.setOnClickListener(mTileClickListener);
    234                 break;
    235             case R.layout.suggestion_header:
    236                 onBindSuggestionHeader(holder, (DashboardData.SuggestionHeaderData)
    237                         mDashboardData.getItemEntityByPosition(position));
    238                 break;
    239             case R.layout.suggestion_tile:
    240                 final Tile suggestion = (Tile) mDashboardData.getItemEntityByPosition(position);
    241                 final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
    242                         mContext, suggestion);
    243                 // This is for cases when a suggestion is dismissed and the next one comes to view
    244                 if (!mSuggestionsShownLogged.contains(suggestionId)) {
    245                     mMetricsFeatureProvider.action(
    246                             mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, suggestionId);
    247                     mSuggestionsShownLogged.add(suggestionId);
    248                 }
    249                 onBindTile(holder, suggestion);
    250                 holder.itemView.setOnClickListener(v -> {
    251                     mMetricsFeatureProvider.action(mContext,
    252                             MetricsEvent.ACTION_SETTINGS_SUGGESTION, suggestionId);
    253                     ((SettingsActivity) mContext).startSuggestion(suggestion.intent);
    254                 });
    255                 break;
    256             case R.layout.condition_card:
    257                 final boolean isExpanded = mDashboardData.getItemEntityByPosition(position)
    258                         == mDashboardData.getExpandedCondition();
    259                 ConditionAdapterUtils.bindViews(
    260                         (Condition) mDashboardData.getItemEntityByPosition(position),
    261                         holder, isExpanded, mConditionClickListener, v -> onExpandClick(v));
    262                 break;
    263         }
    264     }
    265 
    266     @Override
    267     public long getItemId(int position) {
    268         return mDashboardData.getItemIdByPosition(position);
    269     }
    270 
    271     @Override
    272     public int getItemViewType(int position) {
    273         return mDashboardData.getItemTypeByPosition(position);
    274     }
    275 
    276     @Override
    277     public int getItemCount() {
    278         return mDashboardData.size();
    279     }
    280 
    281     public void onPause() {
    282         if (mDashboardData.getSuggestions() == null) {
    283             return;
    284         }
    285         for (Tile suggestion : mDashboardData.getSuggestions()) {
    286             String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
    287                     mContext, suggestion);
    288             if (mSuggestionsShownLogged.contains(suggestionId)) {
    289                 mMetricsFeatureProvider.action(
    290                         mContext, MetricsEvent.ACTION_HIDE_SETTINGS_SUGGESTION, suggestionId);
    291             }
    292         }
    293         mSuggestionsShownLogged.clear();
    294     }
    295 
    296     public void onExpandClick(View v) {
    297         Condition expandedCondition = mDashboardData.getExpandedCondition();
    298         if (v.getTag() == expandedCondition) {
    299             mMetricsFeatureProvider.action(mContext,
    300                     MetricsEvent.ACTION_SETTINGS_CONDITION_COLLAPSE,
    301                     expandedCondition.getMetricsConstant());
    302             expandedCondition = null;
    303         } else {
    304             expandedCondition = (Condition) v.getTag();
    305             mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
    306                     expandedCondition.getMetricsConstant());
    307         }
    308 
    309         updateExpandedCondition(expandedCondition);
    310     }
    311 
    312     public Object getItem(long itemId) {
    313         return mDashboardData.getItemEntityById(itemId);
    314     }
    315 
    316     private void notifyDashboardDataChanged(DashboardData prevData) {
    317         if (mFirstFrameDrawn && prevData != null) {
    318             final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData
    319                     .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList()));
    320             diffResult.dispatchUpdatesTo(this);
    321         } else {
    322             mFirstFrameDrawn = true;
    323             notifyDataSetChanged();
    324         }
    325     }
    326 
    327     private void updateExpandedCondition(Condition condition) {
    328         final DashboardData prevData = mDashboardData;
    329         mDashboardData = new DashboardData.Builder(prevData)
    330                 .setExpandedCondition(condition)
    331                 .build();
    332         notifyDashboardDataChanged(prevData);
    333     }
    334 
    335     @Override
    336     public Tile getSuggestionForPosition(int position) {
    337         return (Tile) mDashboardData.getItemEntityByPosition(position);
    338     }
    339 
    340     @Override
    341     public void onSuggestionDismissed(Tile suggestion) {
    342         final List<Tile> suggestions = mDashboardData.getSuggestions();
    343         if (suggestions == null) {
    344             return;
    345         }
    346         suggestions.remove(suggestion);
    347 
    348         final DashboardData prevData = mDashboardData;
    349         mDashboardData = new DashboardData.Builder(prevData)
    350                 .setSuggestions(suggestions)
    351                 .build();
    352         notifyDashboardDataChanged(prevData);
    353     }
    354 
    355     @VisibleForTesting
    356     void onBindSuggestionHeader(final DashboardItemHolder holder, DashboardData
    357             .SuggestionHeaderData data) {
    358         final boolean moreSuggestions = data.hasMoreSuggestions;
    359         final int undisplayedSuggestionCount = data.undisplayedSuggestionCount;
    360 
    361         holder.icon.setImageResource(moreSuggestions ? R.drawable.ic_expand_more
    362                 : R.drawable.ic_expand_less);
    363         holder.title.setText(mContext.getString(R.string.suggestions_title, data.suggestionSize));
    364         String summaryContentDescription;
    365         if (moreSuggestions) {
    366             summaryContentDescription = mContext.getResources().getQuantityString(
    367                     R.plurals.settings_suggestion_header_summary_hidden_items,
    368                     undisplayedSuggestionCount, undisplayedSuggestionCount);
    369         } else {
    370             summaryContentDescription = mContext.getString(R.string.condition_expand_hide);
    371         }
    372         holder.summary.setContentDescription(summaryContentDescription);
    373 
    374         if (undisplayedSuggestionCount == 0) {
    375             holder.summary.setText(null);
    376         } else {
    377             holder.summary.setText(
    378                     mContext.getString(R.string.suggestions_summary, undisplayedSuggestionCount));
    379         }
    380         holder.itemView.setOnClickListener(v -> {
    381             final int suggestionMode;
    382             if (moreSuggestions) {
    383                 suggestionMode = DashboardData.SUGGESTION_MODE_EXPANDED;
    384 
    385                 for (Tile suggestion : mDashboardData.getSuggestions()) {
    386                     final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
    387                             mContext, suggestion);
    388                     if (!mSuggestionsShownLogged.contains(suggestionId)) {
    389                         mMetricsFeatureProvider.action(
    390                                 mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION,
    391                                 suggestionId);
    392                         mSuggestionsShownLogged.add(suggestionId);
    393                     }
    394                 }
    395             } else {
    396                 suggestionMode = DashboardData.SUGGESTION_MODE_COLLAPSED;
    397             }
    398 
    399             DashboardData prevData = mDashboardData;
    400             mDashboardData = new DashboardData.Builder(prevData)
    401                     .setSuggestionMode(suggestionMode)
    402                     .build();
    403             notifyDashboardDataChanged(prevData);
    404         });
    405     }
    406 
    407     private void onBindTile(DashboardItemHolder holder, Tile tile) {
    408         holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
    409         holder.title.setText(tile.title);
    410         if (!TextUtils.isEmpty(tile.summary)) {
    411             holder.summary.setText(tile.summary);
    412             holder.summary.setVisibility(View.VISIBLE);
    413         } else {
    414             holder.summary.setVisibility(View.GONE);
    415         }
    416     }
    417 
    418     private void onBindCategory(DashboardItemHolder holder, DashboardCategory category) {
    419         holder.title.setText(category.title);
    420     }
    421 
    422     void onSaveInstanceState(Bundle outState) {
    423         final List<Tile> suggestions = mDashboardData.getSuggestions();
    424         final List<DashboardCategory> categories = mDashboardData.getCategories();
    425         if (suggestions != null) {
    426             outState.putParcelableArrayList(STATE_SUGGESTION_LIST, new ArrayList<>(suggestions));
    427         }
    428         if (categories != null) {
    429             outState.putParcelableArrayList(STATE_CATEGORY_LIST, new ArrayList<>(categories));
    430         }
    431         outState.putInt(STATE_SUGGESTION_MODE, mDashboardData.getSuggestionMode());
    432         outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged);
    433     }
    434 
    435     private static class IconCache {
    436         private final Context mContext;
    437         private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();
    438 
    439         public IconCache(Context context) {
    440             mContext = context;
    441         }
    442 
    443         public Drawable getIcon(Icon icon) {
    444             Drawable drawable = mMap.get(icon);
    445             if (drawable == null) {
    446                 drawable = icon.loadDrawable(mContext);
    447                 mMap.put(icon, drawable);
    448             }
    449             return drawable;
    450         }
    451     }
    452 
    453     public static class DashboardItemHolder extends RecyclerView.ViewHolder {
    454         public final ImageView icon;
    455         public final TextView title;
    456         public final TextView summary;
    457 
    458         public DashboardItemHolder(View itemView) {
    459             super(itemView);
    460             icon = itemView.findViewById(android.R.id.icon);
    461             title = itemView.findViewById(android.R.id.title);
    462             summary = itemView.findViewById(android.R.id.summary);
    463         }
    464     }
    465 }
    466