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.pm.PackageManager;
     21 import android.graphics.drawable.Drawable;
     22 import android.os.Bundle;
     23 import android.service.settings.suggestions.Suggestion;
     24 import android.support.annotation.VisibleForTesting;
     25 import android.support.v7.util.DiffUtil;
     26 import android.support.v7.widget.LinearLayoutManager;
     27 import android.support.v7.widget.RecyclerView;
     28 import android.text.TextUtils;
     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.LinearLayout;
     35 import android.widget.TextView;
     36 
     37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     38 import com.android.settings.R;
     39 import com.android.settings.R.id;
     40 import com.android.settings.dashboard.DashboardData.ConditionHeaderData;
     41 import com.android.settings.dashboard.conditional.Condition;
     42 import com.android.settings.dashboard.conditional.ConditionAdapter;
     43 import com.android.settings.dashboard.suggestions.SuggestionAdapter;
     44 import com.android.settings.overlay.FeatureFactory;
     45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
     46 import com.android.settingslib.core.lifecycle.Lifecycle;
     47 import com.android.settingslib.core.lifecycle.LifecycleObserver;
     48 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
     49 import com.android.settingslib.drawer.DashboardCategory;
     50 import com.android.settingslib.drawer.Tile;
     51 import com.android.settingslib.drawer.TileUtils;
     52 import com.android.settingslib.suggestions.SuggestionControllerMixin;
     53 import com.android.settingslib.utils.IconCache;
     54 
     55 import java.util.List;
     56 
     57 public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
     58         implements SummaryLoader.SummaryConsumer, SuggestionAdapter.Callback, LifecycleObserver,
     59         OnSaveInstanceState {
     60     public static final String TAG = "DashboardAdapter";
     61     private static final String STATE_CATEGORY_LIST = "category_list";
     62 
     63     @VisibleForTesting
     64     static final String STATE_CONDITION_EXPANDED = "condition_expanded";
     65 
     66     private final IconCache mCache;
     67     private final Context mContext;
     68     private final MetricsFeatureProvider mMetricsFeatureProvider;
     69     private final DashboardFeatureProvider mDashboardFeatureProvider;
     70     private boolean mFirstFrameDrawn;
     71     private RecyclerView mRecyclerView;
     72     private SuggestionAdapter mSuggestionAdapter;
     73 
     74     @VisibleForTesting
     75     DashboardData mDashboardData;
     76 
     77     private View.OnClickListener mTileClickListener = new View.OnClickListener() {
     78         @Override
     79         public void onClick(View v) {
     80             //TODO: get rid of setTag/getTag
     81             mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag());
     82         }
     83     };
     84 
     85     public DashboardAdapter(Context context, Bundle savedInstanceState,
     86             List<Condition> conditions, SuggestionControllerMixin suggestionControllerMixin,
     87             Lifecycle lifecycle) {
     88 
     89         DashboardCategory category = null;
     90         boolean conditionExpanded = false;
     91 
     92         mContext = context;
     93         final FeatureFactory factory = FeatureFactory.getFactory(context);
     94         mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
     95         mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
     96         mCache = new IconCache(context);
     97         mSuggestionAdapter = new SuggestionAdapter(mContext, suggestionControllerMixin,
     98                 savedInstanceState, this /* callback */, lifecycle);
     99 
    100         setHasStableIds(true);
    101 
    102         if (savedInstanceState != null) {
    103             category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST);
    104             conditionExpanded = savedInstanceState.getBoolean(
    105                     STATE_CONDITION_EXPANDED, conditionExpanded);
    106         }
    107 
    108         if (lifecycle != null) {
    109             lifecycle.addObserver(this);
    110         }
    111 
    112         mDashboardData = new DashboardData.Builder()
    113                 .setConditions(conditions)
    114                 .setSuggestions(mSuggestionAdapter.getSuggestions())
    115                 .setCategory(category)
    116                 .setConditionExpanded(conditionExpanded)
    117                 .build();
    118     }
    119 
    120     public void setSuggestions(List<Suggestion> data) {
    121         final DashboardData prevData = mDashboardData;
    122         mDashboardData = new DashboardData.Builder(prevData)
    123                 .setSuggestions(data)
    124                 .build();
    125         notifyDashboardDataChanged(prevData);
    126     }
    127 
    128     public void setCategory(DashboardCategory category) {
    129         final DashboardData prevData = mDashboardData;
    130         Log.d(TAG, "adapter setCategory called");
    131         mDashboardData = new DashboardData.Builder(prevData)
    132                 .setCategory(category)
    133                 .build();
    134         notifyDashboardDataChanged(prevData);
    135     }
    136 
    137     public void setConditions(List<Condition> conditions) {
    138         final DashboardData prevData = mDashboardData;
    139         Log.d(TAG, "adapter setConditions called");
    140         mDashboardData = new DashboardData.Builder(prevData)
    141                 .setConditions(conditions)
    142                 .build();
    143         notifyDashboardDataChanged(prevData);
    144     }
    145 
    146     @Override
    147     public void onSuggestionClosed(Suggestion suggestion) {
    148         final List<Suggestion> list = mDashboardData.getSuggestions();
    149         if (list == null || list.size() == 0) {
    150             return;
    151         }
    152         if (list.size() == 1) {
    153             // The only suggestion is dismissed, and the the empty suggestion container will
    154             // remain as the dashboard item. Need to refresh the dashboard list.
    155             setSuggestions(null);
    156         } else {
    157             list.remove(suggestion);
    158             setSuggestions(list);
    159         }
    160     }
    161 
    162     @Override
    163     public void notifySummaryChanged(Tile tile) {
    164         final int position = mDashboardData.getPositionByTile(tile);
    165         if (position != DashboardData.POSITION_NOT_FOUND) {
    166             // Since usually tile in parameter and tile in mCategories are same instance,
    167             // which is hard to be detected by DiffUtil, so we notifyItemChanged directly.
    168             notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position));
    169         }
    170     }
    171 
    172     @Override
    173     public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    174         final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
    175         if (viewType == R.layout.condition_header) {
    176             return new ConditionHeaderHolder(view);
    177         }
    178         if (viewType == R.layout.condition_container) {
    179             return new ConditionContainerHolder(view);
    180         }
    181         if (viewType == R.layout.suggestion_container) {
    182             return new SuggestionContainerHolder(view);
    183         }
    184         return new DashboardItemHolder(view);
    185     }
    186 
    187     @Override
    188     public void onBindViewHolder(DashboardItemHolder holder, int position) {
    189         final int type = mDashboardData.getItemTypeByPosition(position);
    190         switch (type) {
    191             case R.layout.dashboard_tile:
    192                 final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position);
    193                 onBindTile(holder, tile);
    194                 holder.itemView.setTag(tile);
    195                 holder.itemView.setOnClickListener(mTileClickListener);
    196                 break;
    197             case R.layout.suggestion_container:
    198                 onBindSuggestion((SuggestionContainerHolder) holder, position);
    199                 break;
    200             case R.layout.condition_container:
    201                 onBindCondition((ConditionContainerHolder) holder, position);
    202                 break;
    203             case R.layout.condition_header:
    204                 onBindConditionHeader((ConditionHeaderHolder) holder,
    205                         (ConditionHeaderData) mDashboardData.getItemEntityByPosition(position));
    206                 break;
    207             case R.layout.condition_footer:
    208                 holder.itemView.setOnClickListener(v -> {
    209                     mMetricsFeatureProvider.action(mContext,
    210                             MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false);
    211                     DashboardData prevData = mDashboardData;
    212                     mDashboardData = new DashboardData.Builder(prevData).
    213                             setConditionExpanded(false).build();
    214                     notifyDashboardDataChanged(prevData);
    215                     scrollToTopOfConditions();
    216                 });
    217                 break;
    218         }
    219     }
    220 
    221     @Override
    222     public long getItemId(int position) {
    223         return mDashboardData.getItemIdByPosition(position);
    224     }
    225 
    226     @Override
    227     public int getItemViewType(int position) {
    228         return mDashboardData.getItemTypeByPosition(position);
    229     }
    230 
    231     @Override
    232     public int getItemCount() {
    233         return mDashboardData.size();
    234     }
    235 
    236     @Override
    237     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    238         super.onAttachedToRecyclerView(recyclerView);
    239         // save the view so that we can scroll it when expanding/collapsing the suggestion and
    240         // conditions.
    241         mRecyclerView = recyclerView;
    242     }
    243 
    244     public Object getItem(long itemId) {
    245         return mDashboardData.getItemEntityById(itemId);
    246     }
    247 
    248     public Suggestion getSuggestion(int position) {
    249         return mSuggestionAdapter.getSuggestion(position);
    250     }
    251 
    252     @VisibleForTesting
    253     void notifyDashboardDataChanged(DashboardData prevData) {
    254         if (mFirstFrameDrawn && prevData != null) {
    255             final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData
    256                     .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList()));
    257             diffResult.dispatchUpdatesTo(this);
    258         } else {
    259             mFirstFrameDrawn = true;
    260             notifyDataSetChanged();
    261         }
    262     }
    263 
    264     @VisibleForTesting
    265     void onBindConditionHeader(final ConditionHeaderHolder holder, ConditionHeaderData data) {
    266         holder.icon.setImageDrawable(data.conditionIcons.get(0));
    267         if (data.conditionCount == 1) {
    268             holder.title.setText(data.title);
    269             holder.summary.setText(null);
    270             holder.icons.setVisibility(View.INVISIBLE);
    271         } else {
    272             holder.title.setText(null);
    273             holder.summary.setText(
    274                     mContext.getString(R.string.condition_summary, data.conditionCount));
    275             updateConditionIcons(data.conditionIcons, holder.icons);
    276             holder.icons.setVisibility(View.VISIBLE);
    277         }
    278 
    279         holder.itemView.setOnClickListener(v -> {
    280             mMetricsFeatureProvider.action(mContext,
    281                     MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true);
    282             final DashboardData prevData = mDashboardData;
    283             mDashboardData = new DashboardData.Builder(prevData)
    284                     .setConditionExpanded(true).build();
    285             notifyDashboardDataChanged(prevData);
    286             scrollToTopOfConditions();
    287         });
    288     }
    289 
    290     @VisibleForTesting
    291     void onBindCondition(final ConditionContainerHolder holder, int position) {
    292         final ConditionAdapter adapter = new ConditionAdapter(mContext,
    293                 (List<Condition>) mDashboardData.getItemEntityByPosition(position),
    294                 mDashboardData.isConditionExpanded());
    295         adapter.addDismissHandling(holder.data);
    296         holder.data.setAdapter(adapter);
    297         holder.data.setLayoutManager(new LinearLayoutManager(mContext));
    298     }
    299 
    300     @VisibleForTesting
    301     void onBindSuggestion(final SuggestionContainerHolder holder, int position) {
    302         // If there is suggestions to show, it will be at position 0 as we don't show the suggestion
    303         // header anymore.
    304         final List<Suggestion> suggestions =
    305                 (List<Suggestion>) mDashboardData.getItemEntityByPosition(position);
    306         if (suggestions != null && suggestions.size() > 0) {
    307             mSuggestionAdapter.setSuggestions(suggestions);
    308             holder.data.setAdapter(mSuggestionAdapter);
    309         }
    310         final LinearLayoutManager layoutManager = new LinearLayoutManager(mContext);
    311         layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
    312         holder.data.setLayoutManager(layoutManager);
    313     }
    314 
    315     @VisibleForTesting
    316     void onBindTile(DashboardItemHolder holder, Tile tile) {
    317         Drawable icon = mCache.getIcon(tile.icon);
    318         if (!TextUtils.equals(tile.icon.getResPackage(), mContext.getPackageName())
    319                 && !(icon instanceof RoundedHomepageIcon)) {
    320             icon = new RoundedHomepageIcon(mContext, icon);
    321             try {
    322                 if (tile.metaData != null) {
    323                     final int colorRes = tile.metaData.getInt(
    324                             TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, 0 /* default */);
    325                     if (colorRes != 0) {
    326                         final int bgColor = mContext.getPackageManager()
    327                                 .getResourcesForApplication(tile.icon.getResPackage())
    328                                 .getColor(colorRes, null /* theme */);
    329                         ((RoundedHomepageIcon) icon).setBackgroundColor(bgColor);
    330                     }
    331                 }
    332             } catch (PackageManager.NameNotFoundException e) {
    333                 Log.e(TAG, "Failed to set background color for " + tile.intent.getPackage());
    334             }
    335             mCache.updateIcon(tile.icon, icon);
    336         }
    337         holder.icon.setImageDrawable(icon);
    338         holder.title.setText(tile.title);
    339         if (!TextUtils.isEmpty(tile.summary)) {
    340             holder.summary.setText(tile.summary);
    341             holder.summary.setVisibility(View.VISIBLE);
    342         } else {
    343             holder.summary.setVisibility(View.GONE);
    344         }
    345     }
    346 
    347     @Override
    348     public void onSaveInstanceState(Bundle outState) {
    349         final DashboardCategory category = mDashboardData.getCategory();
    350         if (category != null) {
    351             outState.putParcelable(STATE_CATEGORY_LIST, category);
    352         }
    353         outState.putBoolean(STATE_CONDITION_EXPANDED, mDashboardData.isConditionExpanded());
    354     }
    355 
    356     private void updateConditionIcons(List<Drawable> icons, ViewGroup parent) {
    357         if (icons == null || icons.size() < 2) {
    358             parent.setVisibility(View.INVISIBLE);
    359             return;
    360         }
    361         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    362         parent.removeAllViews();
    363         for (int i = 1, size = icons.size(); i < size; i++) {
    364             ImageView icon = (ImageView) inflater.inflate(
    365                     R.layout.condition_header_icon, parent, false);
    366             icon.setImageDrawable(icons.get(i));
    367             parent.addView(icon);
    368         }
    369         parent.setVisibility(View.VISIBLE);
    370     }
    371 
    372     private void scrollToTopOfConditions() {
    373         mRecyclerView.scrollToPosition(mDashboardData.hasSuggestion() ? 1 : 0);
    374     }
    375 
    376     public static class DashboardItemHolder extends RecyclerView.ViewHolder {
    377         public final ImageView icon;
    378         public final TextView title;
    379         public final TextView summary;
    380 
    381         public DashboardItemHolder(View itemView) {
    382             super(itemView);
    383             icon = itemView.findViewById(android.R.id.icon);
    384             title = itemView.findViewById(android.R.id.title);
    385             summary = itemView.findViewById(android.R.id.summary);
    386         }
    387     }
    388 
    389     public static class ConditionHeaderHolder extends DashboardItemHolder {
    390         public final LinearLayout icons;
    391         public final ImageView expandIndicator;
    392 
    393         public ConditionHeaderHolder(View itemView) {
    394             super(itemView);
    395             icons = itemView.findViewById(id.additional_icons);
    396             expandIndicator = itemView.findViewById(id.expand_indicator);
    397         }
    398     }
    399 
    400     public static class ConditionContainerHolder extends DashboardItemHolder {
    401         public final RecyclerView data;
    402 
    403         public ConditionContainerHolder(View itemView) {
    404             super(itemView);
    405             data = itemView.findViewById(id.data);
    406         }
    407     }
    408 
    409     public static class SuggestionContainerHolder extends DashboardItemHolder {
    410         public final RecyclerView data;
    411 
    412         public SuggestionContainerHolder(View itemView) {
    413             super(itemView);
    414             data = itemView.findViewById(id.suggestion_list);
    415         }
    416     }
    417 
    418 }
    419