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.service.settings.suggestions.Suggestion;
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 {
43 public static final int HEADER_MODE_DEFAULT = 0;
44 public static final int HEADER_MODE_SUGGESTION_EXPANDED = 1;
45 public static final int HEADER_MODE_FULLY_EXPANDED = 2;
46 public static final int HEADER_MODE_COLLAPSED = 3;
48 @Retention(RetentionPolicy.SOURCE)
49 @IntDef({HEADER_MODE_DEFAULT, HEADER_MODE_SUGGESTION_EXPANDED, HEADER_MODE_FULLY_EXPANDED,
50 HEADER_MODE_COLLAPSED})
51 public @interface HeaderMode {
54 public static final int POSITION_NOT_FOUND = -1;
55 public static final int DEFAULT_SUGGESTION_COUNT = 2;
57 // stable id for different type of items.
59 static final int STABLE_ID_SUGGESTION_CONDITION_TOP_HEADER = 0;
61 static final int STABLE_ID_SUGGESTION_CONDITION_MIDDLE_HEADER = 1;
63 static final int STABLE_ID_SUGGESTION_CONDITION_FOOTER = 2;
65 static final int STABLE_ID_SUGGESTION_CONTAINER = 3;
67 static final int STABLE_ID_CONDITION_CONTAINER = 4;
69 private final List<Item> mItems;
70 private final DashboardCategory mCategory;
71 private final List<Condition> mConditions;
72 private final List<Suggestion> mSuggestions;
74 private final int mSuggestionConditionMode;
76 private DashboardData(Builder builder) {
77 mCategory = builder.mCategory;
78 mConditions = builder.mConditions;
79 mSuggestions = builder.mSuggestionsV2;
80 mSuggestionConditionMode = builder.mSuggestionConditionMode;
82 mItems = new ArrayList<>();
87 public int getItemIdByPosition(int position) {
88 return mItems.get(position).id;
91 public int getItemTypeByPosition(int position) {
92 return mItems.get(position).type;
95 public Object getItemEntityByPosition(int position) {
96 return mItems.get(position).entity;
99 public List<Item> getItemList() {
104 return mItems.size();
107 public Object getItemEntityById(long id) {
108 for (final Item item : mItems) {
116 public DashboardCategory getCategory() {
120 public List<Condition> getConditions() {
124 public List<Suggestion> getSuggestions() {
128 public int getSuggestionConditionMode() {
129 return mSuggestionConditionMode;
133 * Find the position of the object in mItems list, using the equals method to compare
135 * @param entity the object that need to be found in list
136 * @return position of the object, return POSITION_NOT_FOUND if object isn't in the list
138 public int getPositionByEntity(Object entity) {
139 if (entity == null) return POSITION_NOT_FOUND;
141 final int size = mItems.size();
142 for (int i = 0; i < size; i++) {
143 final Object item = mItems.get(i).entity;
144 if (entity.equals(item)) {
149 return POSITION_NOT_FOUND;
153 * Find the position of the Tile object.
155 * First, try to find the exact identical instance of the tile object, if not found,
156 * then try to find a tile has the same title.
158 * @param tile tile that need to be found
159 * @return position of the object, return INDEX_NOT_FOUND if object isn't in the list
161 public int getPositionByTile(Tile tile) {
162 final int size = mItems.size();
163 for (int i = 0; i < size; i++) {
164 final Object entity = mItems.get(i).entity;
165 if (entity == tile) {
167 } else if (entity instanceof Tile && tile.title.equals(((Tile) entity).title)) {
172 return POSITION_NOT_FOUND;
176 * Add item into list when {@paramref add} is true.
178 * @param item maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null
179 * @param type type of the item, and value is the layout id
180 * @param stableId The stable id for this item
181 * @param add flag about whether to add item into list
183 private void addToItemList(Object item, int type, int stableId, boolean add) {
185 mItems.add(new Item(item, type, stableId));
190 * Build the mItems list using mConditions, mSuggestions, mCategories data
191 * and mIsShowingAll, mSuggestionConditionMode flag.
193 private void buildItemsData() {
194 final boolean hasSuggestions = sizeOf(mSuggestions) > 0;
195 final List<Condition> conditions = getConditionsToShow(mConditions);
196 final boolean hasConditions = sizeOf(conditions) > 0;
198 final List<Suggestion> suggestions = getSuggestionsToShow(mSuggestions);
200 final int hiddenSuggestion = hasSuggestions
201 ? sizeOf(mSuggestions) - sizeOf(suggestions)
204 final boolean hasSuggestionAndCollapsed = hasSuggestions
205 && mSuggestionConditionMode == HEADER_MODE_COLLAPSED;
206 final boolean onlyHasConditionAndCollapsed = !hasSuggestions
208 && mSuggestionConditionMode != HEADER_MODE_FULLY_EXPANDED;
210 /* Top suggestion/condition header. This will be present when there is any suggestion
211 * and the mode is collapsed */
212 addToItemList(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
213 R.layout.suggestion_condition_header,
214 STABLE_ID_SUGGESTION_CONDITION_TOP_HEADER, hasSuggestionAndCollapsed);
216 /* Use mid header if there is only condition & it's in collapsed mode */
217 addToItemList(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
218 R.layout.suggestion_condition_header,
219 STABLE_ID_SUGGESTION_CONDITION_MIDDLE_HEADER, onlyHasConditionAndCollapsed);
221 addToItemList(suggestions, R.layout.suggestion_condition_container,
222 STABLE_ID_SUGGESTION_CONTAINER, sizeOf(suggestions) > 0);
224 /* Second suggestion/condition header. This will be added when there is at least one
225 * suggestion or condition that is not currently displayed, and the user can expand the
226 * section to view more items. */
227 addToItemList(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
228 R.layout.suggestion_condition_header,
229 STABLE_ID_SUGGESTION_CONDITION_MIDDLE_HEADER,
230 mSuggestionConditionMode != HEADER_MODE_COLLAPSED
231 && mSuggestionConditionMode != HEADER_MODE_FULLY_EXPANDED
232 && (hiddenSuggestion > 0 || hasConditions && hasSuggestions));
234 /* Condition container. This is the card view that contains the list of conditions.
235 * This will be added whenever the condition list is not empty */
236 addToItemList(conditions, R.layout.suggestion_condition_container,
237 STABLE_ID_CONDITION_CONTAINER,
238 hasConditions && mSuggestionConditionMode == HEADER_MODE_FULLY_EXPANDED);
240 /* Suggestion/condition footer. This will be present when the section is fully expanded
241 * or when there is no conditions and no hidden suggestions */
242 addToItemList(null /* item */, R.layout.suggestion_condition_footer,
243 STABLE_ID_SUGGESTION_CONDITION_FOOTER,
244 (hasConditions || hasSuggestions)
245 && mSuggestionConditionMode == HEADER_MODE_FULLY_EXPANDED
248 && hiddenSuggestion == 0);
250 if (mCategory != null) {
251 final List<Tile> tiles = mCategory.getTiles();
252 for (int j = 0; j < tiles.size(); j++) {
253 final Tile tile = tiles.get(j);
254 addToItemList(tile, R.layout.dashboard_tile, Objects.hash(tile.title),
260 private static int sizeOf(List<?> list) {
261 return list == null ? 0 : list.size();
264 private List<Condition> getConditionsToShow(List<Condition> conditions) {
265 if (conditions == null) {
268 List<Condition> result = new ArrayList<>();
269 final int size = conditions == null ? 0 : conditions.size();
270 for (int i = 0; i < size; i++) {
271 final Condition condition = conditions.get(i);
272 if (condition.shouldShow()) {
273 result.add(condition);
279 private List<Suggestion> getSuggestionsToShow(List<Suggestion> suggestions) {
280 if (suggestions == null || mSuggestionConditionMode == HEADER_MODE_COLLAPSED) {
283 if (mSuggestionConditionMode != HEADER_MODE_DEFAULT
284 || suggestions.size() <= DEFAULT_SUGGESTION_COUNT) {
287 return suggestions.subList(0, DEFAULT_SUGGESTION_COUNT);
291 * Builder used to build the ItemsData
293 * {@link #mSuggestionConditionMode} have default value while others are not.
295 public static class Builder {
297 private int mSuggestionConditionMode = HEADER_MODE_DEFAULT;
299 private DashboardCategory mCategory;
300 private List<Condition> mConditions;
301 private List<Suggestion> mSuggestionsV2;
306 public Builder(DashboardData dashboardData) {
307 mCategory = dashboardData.mCategory;
308 mConditions = dashboardData.mConditions;
309 mSuggestionsV2 = dashboardData.mSuggestions;
310 mSuggestionConditionMode = dashboardData.mSuggestionConditionMode;
313 public Builder setCategory(DashboardCategory category) {
314 this.mCategory = category;
318 public Builder setConditions(List<Condition> conditions) {
319 this.mConditions = conditions;
323 public Builder setSuggestions(List<Suggestion> suggestions) {
324 this.mSuggestionsV2 = suggestions;
328 public Builder setSuggestionConditionMode(@HeaderMode int mode) {
329 this.mSuggestionConditionMode = mode;
333 public DashboardData build() {
334 return new DashboardData(this);
339 * A DiffCallback to calculate the difference between old and new Item
340 * List in DashboardData
342 public static class ItemsDataDiffCallback extends DiffUtil.Callback {
343 final private List<Item> mOldItems;
344 final private List<Item> mNewItems;
346 public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) {
347 mOldItems = oldItems;
348 mNewItems = newItems;
352 public int getOldListSize() {
353 return mOldItems.size();
357 public int getNewListSize() {
358 return mNewItems.size();
362 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
363 return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id;
367 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
368 return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition));
374 * An item contains the data needed in the DashboardData.
377 // valid types in field type
378 private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile;
379 private static final int TYPE_SUGGESTION_CONDITION_CONTAINER =
380 R.layout.suggestion_condition_container;
381 private static final int TYPE_SUGGESTION_CONDITION_HEADER =
382 R.layout.suggestion_condition_header;
383 private static final int TYPE_SUGGESTION_CONDITION_FOOTER =
384 R.layout.suggestion_condition_footer;
385 private static final int TYPE_DASHBOARD_SPACER = R.layout.dashboard_spacer;
387 @IntDef({TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_CONDITION_CONTAINER,
388 TYPE_SUGGESTION_CONDITION_HEADER, TYPE_SUGGESTION_CONDITION_FOOTER,
389 TYPE_DASHBOARD_SPACER})
390 @Retention(RetentionPolicy.SOURCE)
391 public @interface ItemTypes {
395 * The main data object in item, usually is a {@link Tile}, {@link Condition}
396 * object. This object can also be null when the
397 * item is an divider line. Please refer to {@link #buildItemsData()} for
398 * detail usage of the Item.
400 public final Object entity;
403 * The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile)
406 public final int type;
409 * Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item.
413 public Item(Object entity, @ItemTypes int type, int id) {
414 this.entity = entity;
420 * Override it to make comparision in the {@link ItemsDataDiffCallback}
422 * @param obj object to compared with
423 * @return true if the same object or has equal value.
426 public boolean equals(Object obj) {
431 if (!(obj instanceof Item)) {
435 final Item targetItem = (Item) obj;
436 if (type != targetItem.type || id != targetItem.id) {
441 case TYPE_DASHBOARD_TILE:
442 final Tile localTile = (Tile) entity;
443 final Tile targetTile = (Tile) targetItem.entity;
445 // Only check title and summary for dashboard tile
446 return TextUtils.equals(localTile.title, targetTile.title)
447 && TextUtils.equals(localTile.summary, targetTile.summary);
448 case TYPE_SUGGESTION_CONDITION_CONTAINER:
449 // If entity is suggestion and contains remote view, force refresh
450 final List entities = (List) entity;
451 if (!entities.isEmpty()) {
452 Object firstEntity = entities.get(0);
453 if (firstEntity instanceof Tile
454 && ((Tile) firstEntity).remoteViews != null) {
458 // Otherwise Fall through to default
460 return entity == null ? targetItem.entity == null
461 : entity.equals(targetItem.entity);
467 * This class contains the data needed to build the suggestion/condition header. The data can
468 * also be used to check the diff in DiffUtil.Callback
470 public static class SuggestionConditionHeaderData {
471 public final List<Icon> conditionIcons;
472 public final CharSequence title;
473 public final int conditionCount;
474 public final int hiddenSuggestionCount;
476 public SuggestionConditionHeaderData(List<Condition> conditions,
477 int hiddenSuggestionCount) {
478 conditionCount = sizeOf(conditions);
479 this.hiddenSuggestionCount = hiddenSuggestionCount;
480 title = conditionCount > 0 ? conditions.get(0).getTitle() : null;
481 conditionIcons = new ArrayList<>();
482 for (int i = 0; conditions != null && i < conditions.size(); i++) {
483 final Condition condition = conditions.get(i);
484 conditionIcons.add(condition.getIcon());