1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.settings.dashboard;
17
18import android.content.Context;
19import android.content.pm.PackageManager;
20import android.graphics.drawable.Drawable;
21import android.graphics.drawable.Icon;
22import android.os.Bundle;
23import android.support.v7.widget.PopupMenu;
24import android.support.v7.widget.RecyclerView;
25import android.text.TextUtils;
26import android.util.ArrayMap;
27import android.util.TypedValue;
28import android.view.ContextThemeWrapper;
29import android.view.LayoutInflater;
30import android.view.MenuItem;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.ImageView;
34import android.widget.TextView;
35
36import com.android.internal.logging.MetricsLogger;
37import com.android.internal.logging.MetricsProto.MetricsEvent;
38import com.android.internal.util.ArrayUtils;
39import com.android.settings.R;
40import com.android.settings.SettingsActivity;
41import com.android.settings.dashboard.conditional.Condition;
42import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
43import com.android.settingslib.SuggestionParser;
44import com.android.settingslib.drawer.DashboardCategory;
45import com.android.settingslib.drawer.Tile;
46
47import java.util.ArrayList;
48import java.util.List;
49
50public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
51        implements View.OnClickListener {
52    public static final String TAG = "DashboardAdapter";
53    private static final String STATE_SUGGESTION_LIST = "suggestion_list";
54    private static final String STATE_CATEGORY_LIST = "category_list";
55    private static final String STATE_IS_SHOWING_ALL = "is_showing_all";
56    private static final String STATE_SUGGESTION_MODE = "suggestion_mode";
57    private static final int NS_SPACER = 0;
58    private static final int NS_SUGGESTION = 1000;
59    private static final int NS_ITEMS = 2000;
60    private static final int NS_CONDITION = 3000;
61
62    private static int SUGGESTION_MODE_DEFAULT = 0;
63    private static int SUGGESTION_MODE_COLLAPSED = 1;
64    private static int SUGGESTION_MODE_EXPANDED = 2;
65
66    private static final int DEFAULT_SUGGESTION_COUNT = 2;
67
68    private final List<Object> mItems = new ArrayList<>();
69    private final List<Integer> mTypes = new ArrayList<>();
70    private final List<Integer> mIds = new ArrayList<>();
71    private final IconCache mCache;
72
73    private final Context mContext;
74
75    private List<DashboardCategory> mCategories;
76    private List<Condition> mConditions;
77    private List<Tile> mSuggestions;
78
79    private boolean mIsShowingAll;
80    // Used for counting items;
81    private int mId;
82
83    private int mSuggestionMode = SUGGESTION_MODE_DEFAULT;
84
85    private Condition mExpandedCondition = null;
86    private SuggestionParser mSuggestionParser;
87
88    public DashboardAdapter(Context context, SuggestionParser parser, Bundle savedInstanceState,
89                List<Condition> conditions) {
90        mContext = context;
91        mCache = new IconCache(context);
92        mSuggestionParser = parser;
93        mConditions = conditions;
94
95        setHasStableIds(true);
96
97        boolean showAll = true;
98        if (savedInstanceState != null) {
99            mSuggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
100            mCategories = savedInstanceState.getParcelableArrayList(STATE_CATEGORY_LIST);
101            showAll = savedInstanceState.getBoolean(STATE_IS_SHOWING_ALL, true);
102            mSuggestionMode = savedInstanceState.getInt(
103                    STATE_SUGGESTION_MODE, SUGGESTION_MODE_DEFAULT);
104        }
105        setShowingAll(showAll);
106    }
107
108    public List<Tile> getSuggestions() {
109        return mSuggestions;
110    }
111
112    public void setCategoriesAndSuggestions(List<DashboardCategory> categories,
113            List<Tile> suggestions) {
114        mSuggestions = suggestions;
115        mCategories = categories;
116
117        // TODO: Better place for tinting?
118        TypedValue tintColor = new TypedValue();
119        mContext.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
120                tintColor, true);
121        for (int i = 0; i < categories.size(); i++) {
122            for (int j = 0; j < categories.get(i).tiles.size(); j++) {
123                Tile tile = categories.get(i).tiles.get(j);
124
125                if (!mContext.getPackageName().equals(
126                        tile.intent.getComponent().getPackageName())) {
127                    // If this drawable is coming from outside Settings, tint it to match the
128                    // color.
129                    tile.icon.setTint(tintColor.data);
130                }
131            }
132        }
133        recountItems();
134    }
135
136    public void setConditions(List<Condition> conditions) {
137        mConditions = conditions;
138        recountItems();
139    }
140
141    public boolean isShowingAll() {
142        return mIsShowingAll;
143    }
144
145    public void notifyChanged(Tile tile) {
146        notifyDataSetChanged();
147    }
148
149    public void setShowingAll(boolean showingAll) {
150        mIsShowingAll = showingAll;
151        recountItems();
152    }
153
154    private void recountItems() {
155        reset();
156        boolean hasConditions = false;
157        for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
158            boolean shouldShow = mConditions.get(i).shouldShow();
159            hasConditions |= shouldShow;
160            countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
161        }
162        boolean hasSuggestions = mSuggestions != null && mSuggestions.size() != 0;
163        countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
164        countItem(null, R.layout.suggestion_header, hasSuggestions, NS_SPACER);
165        resetCount();
166        if (mSuggestions != null) {
167            int maxSuggestions = getDisplayableSuggestionCount();
168            for (int i = 0; i < mSuggestions.size(); i++) {
169                countItem(mSuggestions.get(i), R.layout.suggestion_tile, i < maxSuggestions,
170                        NS_SUGGESTION);
171            }
172        }
173        resetCount();
174        for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
175            DashboardCategory category = mCategories.get(i);
176            countItem(category, R.layout.dashboard_category, mIsShowingAll, NS_ITEMS);
177            for (int j = 0; j < category.tiles.size(); j++) {
178                Tile tile = category.tiles.get(j);
179                countItem(tile, R.layout.dashboard_tile, mIsShowingAll
180                        || ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
181                        tile.intent.getComponent().getClassName()), NS_ITEMS);
182            }
183        }
184        notifyDataSetChanged();
185    }
186
187    private void resetCount() {
188        mId = 0;
189    }
190
191    private void reset() {
192        mItems.clear();
193        mTypes.clear();
194        mIds.clear();
195        mId = 0;
196    }
197
198    private void countItem(Object object, int type, boolean add, int nameSpace) {
199        if (add) {
200            mItems.add(object);
201            mTypes.add(type);
202            // TODO: Counting namespaces for handling of suggestions/conds appearing/disappearing.
203            mIds.add(mId + nameSpace);
204        }
205        mId++;
206    }
207
208    private int getDisplayableSuggestionCount() {
209        final int suggestionSize = mSuggestions.size();
210        return mSuggestionMode == SUGGESTION_MODE_DEFAULT
211                ? Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize)
212                : mSuggestionMode == SUGGESTION_MODE_EXPANDED
213                        ? suggestionSize : 0;
214    }
215
216    @Override
217    public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
218        return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
219                viewType, parent, false));
220    }
221
222    @Override
223    public void onBindViewHolder(DashboardItemHolder holder, int position) {
224        switch (mTypes.get(position)) {
225            case R.layout.dashboard_category:
226                onBindCategory(holder, (DashboardCategory) mItems.get(position));
227                break;
228            case R.layout.dashboard_tile:
229                final Tile tile = (Tile) mItems.get(position);
230                onBindTile(holder, tile);
231                holder.itemView.setTag(tile);
232                holder.itemView.setOnClickListener(this);
233                break;
234            case R.layout.suggestion_header:
235                onBindSuggestionHeader(holder);
236                break;
237            case R.layout.suggestion_tile:
238                final Tile suggestion = (Tile) mItems.get(position);
239                onBindTile(holder, suggestion);
240                holder.itemView.setOnClickListener(new View.OnClickListener() {
241                    @Override
242                    public void onClick(View v) {
243                        MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_SUGGESTION,
244                                DashboardAdapter.getSuggestionIdentifier(mContext, suggestion));
245                        ((SettingsActivity) mContext).startSuggestion(suggestion.intent);
246                    }
247                });
248                holder.itemView.findViewById(R.id.overflow).setOnClickListener(
249                        new View.OnClickListener() {
250                            @Override
251                            public void onClick(View v) {
252                                showRemoveOption(v, suggestion);
253                            }
254                        });
255                break;
256            case R.layout.see_all:
257                onBindSeeAll(holder);
258                break;
259            case R.layout.condition_card:
260                ConditionAdapterUtils.bindViews((Condition) mItems.get(position), holder,
261                        mItems.get(position) == mExpandedCondition, this,
262                        new View.OnClickListener() {
263                            @Override
264                            public void onClick(View v) {
265                                onExpandClick(v);
266                            }
267                        });
268                break;
269        }
270    }
271
272    private void showRemoveOption(View v, final Tile suggestion) {
273        PopupMenu popup = new PopupMenu(
274                new ContextThemeWrapper(mContext, R.style.Theme_AppCompat_DayNight), v);
275        popup.getMenu().add(R.string.suggestion_remove).setOnMenuItemClickListener(
276                new MenuItem.OnMenuItemClickListener() {
277            @Override
278            public boolean onMenuItemClick(MenuItem item) {
279                MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION,
280                        DashboardAdapter.getSuggestionIdentifier(mContext, suggestion));
281                disableSuggestion(suggestion);
282                mSuggestions.remove(suggestion);
283                recountItems();
284                return true;
285            }
286        });
287        popup.show();
288    }
289
290    public void disableSuggestion(Tile suggestion) {
291        if (mSuggestionParser == null) {
292            return;
293        }
294        if (mSuggestionParser.dismissSuggestion(suggestion)) {
295            mContext.getPackageManager().setComponentEnabledSetting(
296                    suggestion.intent.getComponent(),
297                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
298                    PackageManager.DONT_KILL_APP);
299            mSuggestionParser.markCategoryDone(suggestion.category);
300        }
301    }
302
303    private void onBindSuggestionHeader(final DashboardItemHolder holder) {
304        final boolean moreSuggestions = hasMoreSuggestions();
305        final int undisplayedSuggestionCount =
306                mSuggestions.size() - getDisplayableSuggestionCount();
307        holder.icon.setImageResource(moreSuggestions ? R.drawable.ic_expand_more
308                : R.drawable.ic_expand_less);
309        holder.title.setText(mContext.getString(R.string.suggestions_title, mSuggestions.size()));
310        String summaryContentDescription;
311        if (moreSuggestions) {
312            summaryContentDescription = mContext.getResources().getQuantityString(
313                    R.plurals.settings_suggestion_header_summary_hidden_items,
314                    undisplayedSuggestionCount, undisplayedSuggestionCount);
315        } else {
316            summaryContentDescription = mContext.getString(R.string.condition_expand_hide);
317        }
318        holder.summary.setContentDescription(summaryContentDescription);
319
320        if (undisplayedSuggestionCount == 0) {
321            holder.summary.setText(null);
322        } else {
323            holder.summary.setText(
324                    mContext.getString(R.string.suggestions_summary, undisplayedSuggestionCount));
325        }
326        holder.itemView.setOnClickListener(new View.OnClickListener() {
327            @Override
328            public void onClick(View v) {
329                if (hasMoreSuggestions()) {
330                    mSuggestionMode = SUGGESTION_MODE_EXPANDED;
331                } else {
332                    mSuggestionMode = SUGGESTION_MODE_COLLAPSED;
333                }
334                recountItems();
335            }
336        });
337    }
338
339    private boolean hasMoreSuggestions() {
340        return mSuggestionMode == SUGGESTION_MODE_COLLAPSED
341                || (mSuggestionMode == SUGGESTION_MODE_DEFAULT
342                && mSuggestions.size() > DEFAULT_SUGGESTION_COUNT);
343    }
344
345    private void onBindTile(DashboardItemHolder holder, Tile tile) {
346        holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
347        holder.title.setText(tile.title);
348        if (!TextUtils.isEmpty(tile.summary)) {
349            holder.summary.setText(tile.summary);
350            holder.summary.setVisibility(View.VISIBLE);
351        } else {
352            holder.summary.setVisibility(View.GONE);
353        }
354    }
355
356    private void onBindCategory(DashboardItemHolder holder, DashboardCategory category) {
357        holder.title.setText(category.title);
358    }
359
360    private void onBindSeeAll(DashboardItemHolder holder) {
361        holder.title.setText(mIsShowingAll ? R.string.see_less
362                : R.string.see_all);
363        holder.itemView.setOnClickListener(new View.OnClickListener() {
364            @Override
365            public void onClick(View v) {
366                setShowingAll(!mIsShowingAll);
367            }
368        });
369    }
370
371    @Override
372    public long getItemId(int position) {
373        return mIds.get(position);
374    }
375
376    @Override
377    public int getItemViewType(int position) {
378        return mTypes.get(position);
379    }
380
381    @Override
382    public int getItemCount() {
383        return mIds.size();
384    }
385
386    @Override
387    public void onClick(View v) {
388        if (v.getId() == R.id.dashboard_tile) {
389            ((SettingsActivity) mContext).openTile((Tile) v.getTag());
390            return;
391        }
392        if (v.getTag() == mExpandedCondition) {
393            MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
394                    mExpandedCondition.getMetricsConstant());
395            mExpandedCondition.onPrimaryClick();
396        } else {
397            mExpandedCondition = (Condition) v.getTag();
398            MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
399                    mExpandedCondition.getMetricsConstant());
400            notifyDataSetChanged();
401        }
402    }
403
404    public void onExpandClick(View v) {
405        if (v.getTag() == mExpandedCondition) {
406            MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_COLLAPSE,
407                    mExpandedCondition.getMetricsConstant());
408            mExpandedCondition = null;
409        } else {
410            mExpandedCondition = (Condition) v.getTag();
411            MetricsLogger.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
412                    mExpandedCondition.getMetricsConstant());
413        }
414        notifyDataSetChanged();
415    }
416
417    public Object getItem(long itemId) {
418        for (int i = 0; i < mIds.size(); i++) {
419            if (mIds.get(i) == itemId) {
420                return mItems.get(i);
421            }
422        }
423        return null;
424    }
425
426    public static String getSuggestionIdentifier(Context context, Tile suggestion) {
427        String packageName = suggestion.intent.getComponent().getPackageName();
428        if (packageName.equals(context.getPackageName())) {
429            // Since Settings provides several suggestions, fill in the class instead of the
430            // package for these.
431            packageName = suggestion.intent.getComponent().getClassName();
432        }
433        return packageName;
434    }
435
436    void onSaveInstanceState(Bundle outState) {
437        if (mSuggestions != null) {
438            outState.putParcelableArrayList(STATE_SUGGESTION_LIST,
439                    new ArrayList<Tile>(mSuggestions));
440        }
441        if (mCategories != null) {
442            outState.putParcelableArrayList(STATE_CATEGORY_LIST,
443                    new ArrayList<DashboardCategory>(mCategories));
444        }
445        outState.putBoolean(STATE_IS_SHOWING_ALL, mIsShowingAll);
446        outState.putInt(STATE_SUGGESTION_MODE, mSuggestionMode);
447    }
448
449    private static class IconCache {
450
451        private final Context mContext;
452        private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();
453
454        public IconCache(Context context) {
455            mContext = context;
456        }
457
458        public Drawable getIcon(Icon icon) {
459            Drawable drawable = mMap.get(icon);
460            if (drawable == null) {
461                drawable = icon.loadDrawable(mContext);
462                mMap.put(icon, drawable);
463            }
464            return drawable;
465        }
466    }
467
468    public static class DashboardItemHolder extends RecyclerView.ViewHolder {
469        public final ImageView icon;
470        public final TextView title;
471        public final TextView summary;
472
473        public DashboardItemHolder(View itemView) {
474            super(itemView);
475            icon = (ImageView) itemView.findViewById(android.R.id.icon);
476            title = (TextView) itemView.findViewById(android.R.id.title);
477            summary = (TextView) itemView.findViewById(android.R.id.summary);
478        }
479    }
480}
481