2 * Copyright (C) 2016 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.android.settings.dashboard;
18 import android.annotation.IntDef;
19 import android.graphics.drawable.Icon;
20 import android.support.annotation.Nullable;
21 import android.support.annotation.VisibleForTesting;
22 import android.support.v7.util.DiffUtil;
23 import android.text.TextUtils;
25 import com.android.settings.R;
26 import com.android.settings.dashboard.conditional.Condition;
27 import com.android.settingslib.drawer.DashboardCategory;
28 import com.android.settingslib.drawer.Tile;
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Objects;
37 * Description about data list used in the DashboardAdapter. In the data list each item can be
38 * Condition, suggestion or category tile.
40 * ItemsData has inner class Item, which represents the Item in data list.
42 public class DashboardData {
44 public static final int SUGGESTION_MODE_DEFAULT = 0;
46 public static final int SUGGESTION_MODE_COLLAPSED = 1;
48 public static final int SUGGESTION_MODE_EXPANDED = 2;
50 public static final int HEADER_MODE_DEFAULT = 0;
51 public static final int HEADER_MODE_SUGGESTION_EXPANDED = 1;
52 public static final int HEADER_MODE_FULLY_EXPANDED = 2;
53 public static final int HEADER_MODE_COLLAPSED = 3;
54 @Retention(RetentionPolicy.SOURCE)
55 @IntDef({HEADER_MODE_DEFAULT, HEADER_MODE_SUGGESTION_EXPANDED, HEADER_MODE_FULLY_EXPANDED,
56 HEADER_MODE_COLLAPSED})
57 public @interface HeaderMode{}
59 public static final int POSITION_NOT_FOUND = -1;
60 public static final int DEFAULT_SUGGESTION_COUNT = 2;
62 // id namespace for different type of items.
63 private static final int NS_SPACER = 0;
64 private static final int NS_ITEMS = 2000;
65 private static final int NS_CONDITION = 3000;
66 private static final int NS_SUGGESTION_CONDITION = 4000;
68 private final List<Item> mItems;
69 private final List<DashboardCategory> mCategories;
70 private final List<Condition> mConditions;
71 private final List<Tile> mSuggestions;
73 private final int mSuggestionMode;
75 private final Condition mExpandedCondition;
76 private final @HeaderMode int mSuggestionConditionMode;
78 private boolean mCombineSuggestionAndCondition;
80 private DashboardData(Builder builder) {
81 mCategories = builder.mCategories;
82 mConditions = builder.mConditions;
83 mSuggestions = builder.mSuggestions;
84 mSuggestionMode = builder.mSuggestionMode;
85 mExpandedCondition = builder.mExpandedCondition;
86 mSuggestionConditionMode = builder.mSuggestionConditionMode;
87 mCombineSuggestionAndCondition = builder.mCombineSuggestionAndCondition;
89 mItems = new ArrayList<>();
95 public int getItemIdByPosition(int position) {
96 return mItems.get(position).id;
99 public int getItemTypeByPosition(int position) {
100 return mItems.get(position).type;
103 public Object getItemEntityByPosition(int position) {
104 return mItems.get(position).entity;
107 public List<Item> getItemList() {
112 return mItems.size();
115 public Object getItemEntityById(long id) {
116 for (final Item item : mItems) {
124 public List<DashboardCategory> getCategories() {
128 public List<Condition> getConditions() {
132 public List<Tile> getSuggestions() {
136 public int getSuggestionMode() {
137 return mSuggestionMode;
140 public int getSuggestionConditionMode() {
141 return mSuggestionConditionMode;
145 public Condition getExpandedCondition() {
146 return mExpandedCondition;
150 * Find the position of the object in mItems list, using the equals method to compare
152 * @param entity the object that need to be found in list
153 * @return position of the object, return POSITION_NOT_FOUND if object isn't in the list
155 public int getPositionByEntity(Object entity) {
156 if (entity == null) return POSITION_NOT_FOUND;
158 final int size = mItems.size();
159 for (int i = 0; i < size; i++) {
160 final Object item = mItems.get(i).entity;
161 if (entity.equals(item)) {
166 return POSITION_NOT_FOUND;
170 * Find the position of the Tile object.
172 * First, try to find the exact identical instance of the tile object, if not found,
173 * then try to find a tile has the same title.
175 * @param tile tile that need to be found
176 * @return position of the object, return INDEX_NOT_FOUND if object isn't in the list
178 public int getPositionByTile(Tile tile) {
179 final int size = mItems.size();
180 for (int i = 0; i < size; i++) {
181 final Object entity = mItems.get(i).entity;
182 if (entity == tile) {
184 } else if (entity instanceof Tile && tile.title.equals(((Tile) entity).title)) {
189 return POSITION_NOT_FOUND;
193 * Get the count of suggestions to display
195 * The displayable count mainly depends on the {@link #mSuggestionMode}
196 * and the size of suggestions list.
198 * When in default mode, displayable count couldn't larger than
199 * {@link #DEFAULT_SUGGESTION_COUNT}.
201 * When in expanded mode, display all the suggestions.
203 * @return the count of suggestions to display
205 public int getDisplayableSuggestionCount() {
206 final int suggestionSize = sizeOf(mSuggestions);
207 if (mCombineSuggestionAndCondition) {
208 if (mSuggestionConditionMode == HEADER_MODE_COLLAPSED) {
211 if (mSuggestionConditionMode == HEADER_MODE_DEFAULT) {
212 return Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize);
214 return suggestionSize;
216 if (mSuggestionMode == SUGGESTION_MODE_DEFAULT) {
217 return Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize);
219 if (mSuggestionMode == SUGGESTION_MODE_EXPANDED) {
220 return suggestionSize;
225 public boolean hasMoreSuggestions() {
226 if (mCombineSuggestionAndCondition) {
227 return mSuggestionConditionMode == HEADER_MODE_COLLAPSED && mSuggestions.size() > 0
228 || mSuggestionConditionMode == HEADER_MODE_DEFAULT
229 && mSuggestions.size() > DEFAULT_SUGGESTION_COUNT;
231 return mSuggestionMode == SUGGESTION_MODE_COLLAPSED
232 || (mSuggestionMode == SUGGESTION_MODE_DEFAULT
233 && mSuggestions.size() > DEFAULT_SUGGESTION_COUNT);
236 private void resetCount() {
241 * Count the item and add it into list when {@paramref add} is true.
243 * Note that {@link #mId} will increment automatically and the real
244 * id stored in {@link Item} is shifted by {@paramref nameSpace}. This is a
245 * simple way to keep the id stable.
247 * @param object maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null
248 * @param type type of the item, and value is the layout id
249 * @param add flag about whether to add item into list
250 * @param nameSpace namespace based on the type
252 private void countItem(Object object, int type, boolean add, int nameSpace) {
254 if (mCombineSuggestionAndCondition) {
255 mItems.add(new Item(object, type, mId + nameSpace));
257 mItems.add(new Item(object, type, mId + nameSpace, object == mExpandedCondition));
264 * A special count item method for just suggestions. Id is calculated using suggestion hash
265 * instead of the position of suggestion in list. This is a more stable id than countItem.
267 private void countSuggestion(Tile tile, boolean add) {
271 tile.remoteViews != null
272 ? R.layout.suggestion_tile_card
273 : R.layout.suggestion_tile,
274 Objects.hash(tile.title),
281 * Build the mItems list using mConditions, mSuggestions, mCategories data
282 * and mIsShowingAll, mSuggestionMode flag.
284 private void buildItemsData() {
285 final boolean hasSuggestions = sizeOf(mSuggestions) > 0;
286 if (!mCombineSuggestionAndCondition) {
287 boolean hasConditions = false;
288 for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
289 boolean shouldShow = mConditions.get(i).shouldShow();
290 hasConditions |= shouldShow;
291 countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
295 countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
296 countItem(buildSuggestionHeaderData(), R.layout.suggestion_header, hasSuggestions,
300 if (mSuggestions != null) {
301 int maxSuggestions = getDisplayableSuggestionCount();
302 for (int i = 0; i < mSuggestions.size(); i++) {
303 countSuggestion(mSuggestions.get(i), i < maxSuggestions);
307 final List<Condition> conditions = getConditionsToShow(mConditions);
308 final boolean hasConditions = sizeOf(conditions) > 0;
310 final List<Tile> suggestions = getSuggestionsToShow(mSuggestions);
311 final int hiddenSuggestion =
312 hasSuggestions ? sizeOf(mSuggestions) - sizeOf(suggestions) : 0;
315 /* Top suggestion/condition header. This will be present when there is any suggestion or
316 * condition to show, except in the case that there is only conditions to show and the
317 * mode is fully expanded. */
318 countItem(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
319 R.layout.suggestion_condition_header, hasSuggestions
320 || hasConditions && mSuggestionConditionMode != HEADER_MODE_FULLY_EXPANDED,
321 NS_SUGGESTION_CONDITION);
323 /* Suggestion container. This is the card view that contains the list of suggestions.
324 * This will be added whenever the suggestion list is not empty */
325 countItem(suggestions, R.layout.suggestion_condition_container, sizeOf(suggestions) > 0,
326 NS_SUGGESTION_CONDITION);
328 /* Second suggestion/condition header. This will be added when there is at least one
329 * suggestion or condition that is not currently displayed, and the user can expand the
330 * section to view more items. */
331 countItem(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
332 R.layout.suggestion_condition_header,
333 mSuggestionConditionMode != HEADER_MODE_COLLAPSED
334 && mSuggestionConditionMode != HEADER_MODE_FULLY_EXPANDED
335 && (hiddenSuggestion > 0
336 || hasConditions && hasSuggestions),
337 NS_SUGGESTION_CONDITION);
339 /* Condition container. This is the card view that contains the list of conditions.
340 * This will be added whenever the condition list is not empty */
341 countItem(conditions, R.layout.suggestion_condition_container,
342 hasConditions && mSuggestionConditionMode == HEADER_MODE_FULLY_EXPANDED,
343 NS_SUGGESTION_CONDITION);
345 /* Suggestion/condition footer. This will be present when the section is fully expanded
346 * or when there is no conditions and no hidden suggestions */
347 countItem(null, R.layout.suggestion_condition_footer,
348 (hasConditions || hasSuggestions) &&
349 mSuggestionConditionMode == HEADER_MODE_FULLY_EXPANDED
350 || hasSuggestions && !hasConditions && hiddenSuggestion == 0,
351 NS_SUGGESTION_CONDITION);
355 for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
356 DashboardCategory category = mCategories.get(i);
357 countItem(category, R.layout.dashboard_category,
358 !TextUtils.isEmpty(category.title), NS_ITEMS);
359 for (int j = 0; j < category.tiles.size(); j++) {
360 Tile tile = category.tiles.get(j);
361 countItem(tile, R.layout.dashboard_tile, true, NS_ITEMS);
366 private static int sizeOf(List<?> list) {
367 return list == null ? 0 : list.size();
370 private SuggestionHeaderData buildSuggestionHeaderData() {
371 SuggestionHeaderData data;
372 if (mSuggestions == null) {
373 data = new SuggestionHeaderData();
375 final boolean hasMoreSuggestions = hasMoreSuggestions();
376 final int suggestionSize = mSuggestions.size();
377 final int undisplayedSuggestionCount = suggestionSize - getDisplayableSuggestionCount();
378 data = new SuggestionHeaderData(hasMoreSuggestions, suggestionSize,
379 undisplayedSuggestionCount);
385 private List<Condition> getConditionsToShow(List<Condition> conditions) {
386 if (conditions == null) {
389 List<Condition> result = new ArrayList<Condition>();
390 final int size = conditions == null ? 0 : conditions.size();
391 for (int i = 0; i < size; i++) {
392 final Condition condition = conditions.get(i);
393 if (condition.shouldShow()) {
394 result.add(condition);
400 private List<Tile> getSuggestionsToShow(List<Tile> suggestions) {
401 if (suggestions == null || mSuggestionConditionMode == HEADER_MODE_COLLAPSED) {
404 if (mSuggestionConditionMode != HEADER_MODE_DEFAULT
405 || suggestions.size() <= DEFAULT_SUGGESTION_COUNT) {
408 return suggestions.subList(0, DEFAULT_SUGGESTION_COUNT);
412 * Builder used to build the ItemsData
414 * {@link #mExpandedCondition}, {@link #mSuggestionConditionMode} and {@link #mSuggestionMode}
415 * have default value while others are not.
417 public static class Builder {
419 private int mSuggestionMode = SUGGESTION_MODE_DEFAULT;
421 private Condition mExpandedCondition = null;
422 private @HeaderMode int mSuggestionConditionMode = HEADER_MODE_DEFAULT;
424 private List<DashboardCategory> mCategories;
425 private List<Condition> mConditions;
426 private List<Tile> mSuggestions;
427 private boolean mCombineSuggestionAndCondition;
432 public Builder(DashboardData dashboardData) {
433 mCategories = dashboardData.mCategories;
434 mConditions = dashboardData.mConditions;
435 mSuggestions = dashboardData.mSuggestions;
436 mSuggestionMode = dashboardData.mSuggestionMode;
437 mExpandedCondition = dashboardData.mExpandedCondition;
438 mSuggestionConditionMode = dashboardData.mSuggestionConditionMode;
439 mCombineSuggestionAndCondition = dashboardData.mCombineSuggestionAndCondition;
442 public Builder setCategories(List<DashboardCategory> categories) {
443 this.mCategories = categories;
447 public Builder setConditions(List<Condition> conditions) {
448 this.mConditions = conditions;
452 public Builder setSuggestions(List<Tile> suggestions) {
453 this.mSuggestions = suggestions;
457 public Builder setSuggestionMode(int suggestionMode) {
458 this.mSuggestionMode = suggestionMode;
463 public Builder setExpandedCondition(Condition expandedCondition) {
464 this.mExpandedCondition = expandedCondition;
468 public Builder setSuggestionConditionMode(@HeaderMode int mode) {
469 this.mSuggestionConditionMode = mode;
473 public Builder setCombineSuggestionAndCondition(boolean combine) {
474 this.mCombineSuggestionAndCondition = combine;
478 public DashboardData build() {
479 return new DashboardData(this);
484 * A DiffCallback to calculate the difference between old and new Item
485 * List in DashboardData
487 public static class ItemsDataDiffCallback extends DiffUtil.Callback {
488 final private List<Item> mOldItems;
489 final private List<Item> mNewItems;
491 public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) {
492 mOldItems = oldItems;
493 mNewItems = newItems;
497 public int getOldListSize() {
498 return mOldItems.size();
502 public int getNewListSize() {
503 return mNewItems.size();
507 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
508 return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id;
512 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
513 return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition));
516 // not needed in combined UI
520 public Object getChangePayload(int oldItemPosition, int newItemPosition) {
521 if (mOldItems.get(oldItemPosition).type == Item.TYPE_CONDITION_CARD) {
522 return "condition"; // return anything but null to mark the payload
529 * An item contains the data needed in the DashboardData.
531 private static class Item {
532 // valid types in field type
533 private static final int TYPE_DASHBOARD_CATEGORY = R.layout.dashboard_category;
534 private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile;
536 private static final int TYPE_SUGGESTION_HEADER = R.layout.suggestion_header;
538 private static final int TYPE_SUGGESTION_TILE = R.layout.suggestion_tile;
539 private static final int TYPE_SUGGESTION_CONDITION_CONTAINER =
540 R.layout.suggestion_condition_container;
541 private static final int TYPE_SUGGESTION_CONDITION_HEADER =
542 R.layout.suggestion_condition_header;
544 private static final int TYPE_CONDITION_CARD = R.layout.condition_card;
545 private static final int TYPE_SUGGESTION_CONDITION_FOOTER =
546 R.layout.suggestion_condition_footer;
547 private static final int TYPE_DASHBOARD_SPACER = R.layout.dashboard_spacer;
549 @IntDef({TYPE_DASHBOARD_CATEGORY, TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_HEADER,
550 TYPE_SUGGESTION_TILE, TYPE_SUGGESTION_CONDITION_CONTAINER,
551 TYPE_SUGGESTION_CONDITION_HEADER, TYPE_CONDITION_CARD,
552 TYPE_SUGGESTION_CONDITION_FOOTER, TYPE_DASHBOARD_SPACER})
553 @Retention(RetentionPolicy.SOURCE)
554 public @interface ItemTypes{}
557 * The main data object in item, usually is a {@link Tile}, {@link Condition} or
558 * {@link DashboardCategory} object. This object can also be null when the
559 * item is an divider line. Please refer to {@link #buildItemsData()} for
560 * detail usage of the Item.
562 public final Object entity;
565 * The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile)
567 public final @ItemTypes int type;
570 * Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item.
575 * To store whether the condition is expanded, useless when {@link #type} is not
576 * {@link #TYPE_CONDITION_CARD}
579 public final boolean conditionExpanded;
582 public Item(Object entity, @ItemTypes int type, int id, boolean conditionExpanded) {
583 this.entity = entity;
586 this.conditionExpanded = conditionExpanded;
589 public Item(Object entity, @ItemTypes int type, int id) {
590 this(entity, type, id, false);
594 * Override it to make comparision in the {@link ItemsDataDiffCallback}
595 * @param obj object to compared with
596 * @return true if the same object or has equal value.
599 public boolean equals(Object obj) {
604 if (!(obj instanceof Item)) {
608 final Item targetItem = (Item) obj;
609 if (type != targetItem.type || id != targetItem.id) {
614 case TYPE_DASHBOARD_CATEGORY:
615 // Only check title for dashboard category
616 return TextUtils.equals(((DashboardCategory) entity).title,
617 ((DashboardCategory) targetItem.entity).title);
618 case TYPE_DASHBOARD_TILE:
619 final Tile localTile = (Tile) entity;
620 final Tile targetTile = (Tile) targetItem.entity;
622 // Only check title and summary for dashboard tile
623 return TextUtils.equals(localTile.title, targetTile.title)
624 && TextUtils.equals(localTile.summary, targetTile.summary);
625 case TYPE_CONDITION_CARD:
626 // First check conditionExpanded for quick return
627 if (conditionExpanded != targetItem.conditionExpanded) {
630 // After that, go to default to do final check
632 return entity == null ? targetItem.entity == null
633 : entity.equals(targetItem.entity);
639 * This class contains the data needed to build the header. The data can also be
640 * used to check the diff in DiffUtil.Callback
642 public static class SuggestionHeaderData {
643 public final boolean hasMoreSuggestions;
644 public final int suggestionSize;
645 public final int undisplayedSuggestionCount;
647 public SuggestionHeaderData(boolean moreSuggestions, int suggestionSize, int
648 undisplayedSuggestionCount) {
649 this.hasMoreSuggestions = moreSuggestions;
650 this.suggestionSize = suggestionSize;
651 this.undisplayedSuggestionCount = undisplayedSuggestionCount;
654 public SuggestionHeaderData() {
655 hasMoreSuggestions = false;
657 undisplayedSuggestionCount = 0;
661 public boolean equals(Object obj) {
666 if (!(obj instanceof SuggestionHeaderData)) {
670 SuggestionHeaderData targetData = (SuggestionHeaderData) obj;
672 return hasMoreSuggestions == targetData.hasMoreSuggestions
673 && suggestionSize == targetData.suggestionSize
674 && undisplayedSuggestionCount == targetData.undisplayedSuggestionCount;
679 * This class contains the data needed to build the suggestion/condition header. The data can
680 * also be used to check the diff in DiffUtil.Callback
682 public static class SuggestionConditionHeaderData {
683 public final List<Icon> conditionIcons;
684 public final CharSequence title;
685 public final int conditionCount;
686 public final int hiddenSuggestionCount;
688 public SuggestionConditionHeaderData(List<Condition> conditions,
689 int hiddenSuggestionCount) {
690 conditionCount = sizeOf(conditions);
691 this.hiddenSuggestionCount = hiddenSuggestionCount;
692 title = conditionCount > 0 ? conditions.get(0).getTitle() : null;
693 conditionIcons = new ArrayList<Icon>();
694 for (int i = 0; conditions != null && i < conditions.size(); i++) {
695 final Condition condition = conditions.get(i);
696 conditionIcons.add(condition.getIcon());