1 /*
2  * Copyright (C) 2016 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.annotation.IntDef;
19 import android.support.annotation.Nullable;
20 import android.support.v7.util.DiffUtil;
21 import android.text.TextUtils;
22 
23 import com.android.settings.R;
24 import com.android.settings.dashboard.conditional.Condition;
25 import com.android.settingslib.drawer.DashboardCategory;
26 import com.android.settingslib.drawer.Tile;
27 
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Objects;
33 
34 /**
35  * Description about data list used in the DashboardAdapter. In the data list each item can be
36  * Condition, suggestion or category tile.
37  * <p>
38  * ItemsData has inner class Item, which represents the Item in data list.
39  */
40 public class DashboardData {
41     public static final int SUGGESTION_MODE_DEFAULT = 0;
42     public static final int SUGGESTION_MODE_COLLAPSED = 1;
43     public static final int SUGGESTION_MODE_EXPANDED = 2;
44     public static final int POSITION_NOT_FOUND = -1;
45     public static final int DEFAULT_SUGGESTION_COUNT = 2;
46 
47     // id namespace for different type of items.
48     private static final int NS_SPACER = 0;
49     private static final int NS_ITEMS = 2000;
50     private static final int NS_CONDITION = 3000;
51 
52     private final List<Item> mItems;
53     private final List<DashboardCategory> mCategories;
54     private final List<Condition> mConditions;
55     private final List<Tile> mSuggestions;
56     private final int mSuggestionMode;
57     private final Condition mExpandedCondition;
58     private int mId;
59 
DashboardData(Builder builder)60     private DashboardData(Builder builder) {
61         mCategories = builder.mCategories;
62         mConditions = builder.mConditions;
63         mSuggestions = builder.mSuggestions;
64         mSuggestionMode = builder.mSuggestionMode;
65         mExpandedCondition = builder.mExpandedCondition;
66 
67         mItems = new ArrayList<>();
68         mId = 0;
69 
70         buildItemsData();
71     }
72 
getItemIdByPosition(int position)73     public int getItemIdByPosition(int position) {
74         return mItems.get(position).id;
75     }
76 
getItemTypeByPosition(int position)77     public int getItemTypeByPosition(int position) {
78         return mItems.get(position).type;
79     }
80 
getItemEntityByPosition(int position)81     public Object getItemEntityByPosition(int position) {
82         return mItems.get(position).entity;
83     }
84 
getItemList()85     public List<Item> getItemList() {
86         return mItems;
87     }
88 
size()89     public int size() {
90         return mItems.size();
91     }
92 
getItemEntityById(long id)93     public Object getItemEntityById(long id) {
94         for (final Item item : mItems) {
95             if (item.id == id) {
96                 return item.entity;
97             }
98         }
99         return null;
100     }
101 
getCategories()102     public List<DashboardCategory> getCategories() {
103         return mCategories;
104     }
105 
getConditions()106     public List<Condition> getConditions() {
107         return mConditions;
108     }
109 
getSuggestions()110     public List<Tile> getSuggestions() {
111         return mSuggestions;
112     }
113 
getSuggestionMode()114     public int getSuggestionMode() {
115         return mSuggestionMode;
116     }
117 
getExpandedCondition()118     public Condition getExpandedCondition() {
119         return mExpandedCondition;
120     }
121 
122     /**
123      * Find the position of the object in mItems list, using the equals method to compare
124      *
125      * @param entity the object that need to be found in list
126      * @return position of the object, return POSITION_NOT_FOUND if object isn't in the list
127      */
getPositionByEntity(Object entity)128     public int getPositionByEntity(Object entity) {
129         if (entity == null) return POSITION_NOT_FOUND;
130 
131         final int size = mItems.size();
132         for (int i = 0; i < size; i++) {
133             final Object item = mItems.get(i).entity;
134             if (entity.equals(item)) {
135                 return i;
136             }
137         }
138 
139         return POSITION_NOT_FOUND;
140     }
141 
142     /**
143      * Find the position of the Tile object.
144      * <p>
145      * First, try to find the exact identical instance of the tile object, if not found,
146      * then try to find a tile has the same title.
147      *
148      * @param tile tile that need to be found
149      * @return position of the object, return INDEX_NOT_FOUND if object isn't in the list
150      */
getPositionByTile(Tile tile)151     public int getPositionByTile(Tile tile) {
152         final int size = mItems.size();
153         for (int i = 0; i < size; i++) {
154             final Object entity = mItems.get(i).entity;
155             if (entity == tile) {
156                 return i;
157             } else if (entity instanceof Tile && tile.title.equals(((Tile) entity).title)) {
158                 return i;
159             }
160         }
161 
162         return POSITION_NOT_FOUND;
163     }
164 
165     /**
166      * Get the count of suggestions to display
167      *
168      * The displayable count mainly depends on the {@link #mSuggestionMode}
169      * and the size of suggestions list.
170      *
171      * When in default mode, displayable count couldn't larger than
172      * {@link #DEFAULT_SUGGESTION_COUNT}.
173      *
174      * When in expanded mode, display all the suggestions.
175      *
176      * @return the count of suggestions to display
177      */
getDisplayableSuggestionCount()178     public int getDisplayableSuggestionCount() {
179         final int suggestionSize = mSuggestions.size();
180         return mSuggestionMode == SUGGESTION_MODE_DEFAULT
181                 ? Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize)
182                 : mSuggestionMode == SUGGESTION_MODE_EXPANDED
183                         ? suggestionSize : 0;
184     }
185 
hasMoreSuggestions()186     public boolean hasMoreSuggestions() {
187         return mSuggestionMode == SUGGESTION_MODE_COLLAPSED
188                 || (mSuggestionMode == SUGGESTION_MODE_DEFAULT
189                 && mSuggestions.size() > DEFAULT_SUGGESTION_COUNT);
190     }
191 
resetCount()192     private void resetCount() {
193         mId = 0;
194     }
195 
196     /**
197      * Count the item and add it into list when {@paramref add} is true.
198      *
199      * Note that {@link #mId} will increment automatically and the real
200      * id stored in {@link Item} is shifted by {@paramref nameSpace}. This is a
201      * simple way to keep the id stable.
202      *
203      * @param object    maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null
204      * @param type      type of the item, and value is the layout id
205      * @param add       flag about whether to add item into list
206      * @param nameSpace namespace based on the type
207      */
countItem(Object object, int type, boolean add, int nameSpace)208     private void countItem(Object object, int type, boolean add, int nameSpace) {
209         if (add) {
210             mItems.add(new Item(object, type, mId + nameSpace, object == mExpandedCondition));
211         }
212         mId++;
213     }
214 
215     /**
216      * A special count item method for just suggestions. Id is calculated using suggestion hash
217      * instead of the position of suggestion in list. This is a more stable id than countItem.
218      */
countSuggestion(Tile tile, boolean add)219     private void countSuggestion(Tile tile, boolean add) {
220         if (add) {
221             mItems.add(new Item(tile, R.layout.suggestion_tile, Objects.hash(tile.title), false));
222         }
223         mId++;
224     }
225 
226     /**
227      * Build the mItems list using mConditions, mSuggestions, mCategories data
228      * and mIsShowingAll, mSuggestionMode flag.
229      */
buildItemsData()230     private void buildItemsData() {
231         boolean hasConditions = false;
232         for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
233             boolean shouldShow = mConditions.get(i).shouldShow();
234             hasConditions |= shouldShow;
235             countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
236         }
237 
238         resetCount();
239         final boolean hasSuggestions = mSuggestions != null && mSuggestions.size() != 0;
240         countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
241         countItem(buildSuggestionHeaderData(), R.layout.suggestion_header, hasSuggestions,
242                 NS_SPACER);
243 
244         resetCount();
245         if (mSuggestions != null) {
246             int maxSuggestions = getDisplayableSuggestionCount();
247             for (int i = 0; i < mSuggestions.size(); i++) {
248                 countSuggestion(mSuggestions.get(i), i < maxSuggestions);
249             }
250         }
251         resetCount();
252         for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
253             DashboardCategory category = mCategories.get(i);
254             countItem(category, R.layout.dashboard_category,
255                     !TextUtils.isEmpty(category.title), NS_ITEMS);
256             for (int j = 0; j < category.tiles.size(); j++) {
257                 Tile tile = category.tiles.get(j);
258                 countItem(tile, R.layout.dashboard_tile, true, NS_ITEMS);
259             }
260         }
261     }
262 
263     private SuggestionHeaderData buildSuggestionHeaderData() {
264         SuggestionHeaderData data;
265         if (mSuggestions == null) {
266             data = new SuggestionHeaderData();
267         } else {
268             final boolean hasMoreSuggestions = hasMoreSuggestions();
269             final int suggestionSize = mSuggestions.size();
270             final int undisplayedSuggestionCount = suggestionSize - getDisplayableSuggestionCount();
271             data = new SuggestionHeaderData(hasMoreSuggestions, suggestionSize,
272                     undisplayedSuggestionCount);
273         }
274 
275         return data;
276     }
277 
278     /**
279      * Builder used to build the ItemsData
280      * <p>
281      * {@link #mExpandedCondition} and {@link #mSuggestionMode} have default value
282      * while others are not.
283      */
284     public static class Builder {
285         private int mSuggestionMode = SUGGESTION_MODE_DEFAULT;
286         private Condition mExpandedCondition = null;
287 
288         private List<DashboardCategory> mCategories;
289         private List<Condition> mConditions;
290         private List<Tile> mSuggestions;
291 
292         public Builder() {
293         }
294 
295         public Builder(DashboardData dashboardData) {
296             mCategories = dashboardData.mCategories;
297             mConditions = dashboardData.mConditions;
298             mSuggestions = dashboardData.mSuggestions;
299             mSuggestionMode = dashboardData.mSuggestionMode;
300             mExpandedCondition = dashboardData.mExpandedCondition;
301         }
302 
303         public Builder setCategories(List<DashboardCategory> categories) {
304             this.mCategories = categories;
305             return this;
306         }
307 
308         public Builder setConditions(List<Condition> conditions) {
309             this.mConditions = conditions;
310             return this;
311         }
312 
313         public Builder setSuggestions(List<Tile> suggestions) {
314             this.mSuggestions = suggestions;
315             return this;
316         }
317 
318         public Builder setSuggestionMode(int suggestionMode) {
319             this.mSuggestionMode = suggestionMode;
320             return this;
321         }
322 
323         public Builder setExpandedCondition(Condition expandedCondition) {
324             this.mExpandedCondition = expandedCondition;
325             return this;
326         }
327 
328         public DashboardData build() {
329             return new DashboardData(this);
330         }
331     }
332 
333     /**
334      * A DiffCallback to calculate the difference between old and new Item
335      * List in DashboardData
336      */
337     public static class ItemsDataDiffCallback extends DiffUtil.Callback {
338         final private List<Item> mOldItems;
339         final private List<Item> mNewItems;
340 
341         public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) {
342             mOldItems = oldItems;
343             mNewItems = newItems;
344         }
345 
346         @Override
347         public int getOldListSize() {
348             return mOldItems.size();
349         }
350 
351         @Override
352         public int getNewListSize() {
353             return mNewItems.size();
354         }
355 
356         @Override
357         public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
358             return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id;
359         }
360 
361         @Override
362         public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
363             return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition));
364         }
365 
366         @Nullable
367         @Override
368         public Object getChangePayload(int oldItemPosition, int newItemPosition) {
369             if (mOldItems.get(oldItemPosition).type == Item.TYPE_CONDITION_CARD) {
370                 return "condition"; // return anything but null to mark the payload
371             }
372             return null;
373         }
374     }
375 
376     /**
377      * An item contains the data needed in the DashboardData.
378      */
379     private static class Item {
380         // valid types in field type
381         private static final int TYPE_DASHBOARD_CATEGORY = R.layout.dashboard_category;
382         private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile;
383         private static final int TYPE_SUGGESTION_HEADER = R.layout.suggestion_header;
384         private static final int TYPE_SUGGESTION_TILE = R.layout.suggestion_tile;
385         private static final int TYPE_CONDITION_CARD = R.layout.condition_card;
386         private static final int TYPE_DASHBOARD_SPACER = R.layout.dashboard_spacer;
387 
388         @IntDef({TYPE_DASHBOARD_CATEGORY, TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_HEADER,
389                 TYPE_SUGGESTION_TILE, TYPE_CONDITION_CARD, TYPE_DASHBOARD_SPACER})
390         @Retention(RetentionPolicy.SOURCE)
391         public @interface ItemTypes{}
392 
393         /**
394          * The main data object in item, usually is a {@link Tile}, {@link Condition} or
395          * {@link DashboardCategory} object. This object can also be null when the
396          * item is an divider line. Please refer to {@link #buildItemsData()} for
397          * detail usage of the Item.
398          */
399         public final Object entity;
400 
401         /**
402          * The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile)
403          */
404         public final @ItemTypes int type;
405 
406         /**
407          * Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item.
408          */
409         public final int id;
410 
411         /**
412          * To store whether the condition is expanded, useless when {@link #type} is not
413          * {@link #TYPE_CONDITION_CARD}
414          */
415         public final boolean conditionExpanded;
416 
417         public Item(Object entity, @ItemTypes int type, int id, boolean conditionExpanded) {
418             this.entity = entity;
419             this.type = type;
420             this.id = id;
421             this.conditionExpanded = conditionExpanded;
422         }
423 
424         /**
425          * Override it to make comparision in the {@link ItemsDataDiffCallback}
426          * @param obj object to compared with
427          * @return true if the same object or has equal value.
428          */
429         @Override
430         public boolean equals(Object obj) {
431             if (this == obj) {
432                 return true;
433             }
434 
435             if (!(obj instanceof Item)) {
436                 return false;
437             }
438 
439             final Item targetItem = (Item) obj;
440             if (type != targetItem.type || id != targetItem.id) {
441                 return false;
442             }
443 
444             switch (type) {
445                 case TYPE_DASHBOARD_CATEGORY:
446                     // Only check title for dashboard category
447                     return TextUtils.equals(((DashboardCategory) entity).title,
448                             ((DashboardCategory) targetItem.entity).title);
449                 case TYPE_DASHBOARD_TILE:
450                     final Tile localTile = (Tile) entity;
451                     final Tile targetTile = (Tile) targetItem.entity;
452 
453                     // Only check title and summary for dashboard tile
454                     return TextUtils.equals(localTile.title, targetTile.title)
455                             && TextUtils.equals(localTile.summary, targetTile.summary);
456                 case TYPE_CONDITION_CARD:
457                     // First check conditionExpanded for quick return
458                     if (conditionExpanded != targetItem.conditionExpanded) {
459                         return false;
460                     }
461                     // After that, go to default to do final check
462                 default:
463                     return entity == null ? targetItem.entity == null
464                             : entity.equals(targetItem.entity);
465             }
466         }
467     }
468 
469     /**
470      * This class contains the data needed to build the header. The data can also be
471      * used to check the diff in DiffUtil.Callback
472      */
473     public static class SuggestionHeaderData {
474         public final boolean hasMoreSuggestions;
475         public final int suggestionSize;
476         public final int undisplayedSuggestionCount;
477 
478         public SuggestionHeaderData(boolean moreSuggestions, int suggestionSize, int
479                 undisplayedSuggestionCount) {
480             this.hasMoreSuggestions = moreSuggestions;
481             this.suggestionSize = suggestionSize;
482             this.undisplayedSuggestionCount = undisplayedSuggestionCount;
483         }
484 
485         public SuggestionHeaderData() {
486             hasMoreSuggestions = false;
487             suggestionSize = 0;
488             undisplayedSuggestionCount = 0;
489         }
490 
491         @Override
492         public boolean equals(Object obj) {
493             if (this == obj) {
494                 return true;
495             }
496 
497             if (!(obj instanceof SuggestionHeaderData)) {
498                 return false;
499             }
500 
501             SuggestionHeaderData targetData = (SuggestionHeaderData) obj;
502 
503             return hasMoreSuggestions == targetData.hasMoreSuggestions
504                     && suggestionSize == targetData.suggestionSize
505                     && undisplayedSuggestionCount == targetData.undisplayedSuggestionCount;
506         }
507     }
508 
509 }