Home | History | Annotate | Download | only in dashboard
      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 
     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 
     73     public int getItemIdByPosition(int position) {
     74         return mItems.get(position).id;
     75     }
     76 
     77     public int getItemTypeByPosition(int position) {
     78         return mItems.get(position).type;
     79     }
     80 
     81     public Object getItemEntityByPosition(int position) {
     82         return mItems.get(position).entity;
     83     }
     84 
     85     public List<Item> getItemList() {
     86         return mItems;
     87     }
     88 
     89     public int size() {
     90         return mItems.size();
     91     }
     92 
     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 
    102     public List<DashboardCategory> getCategories() {
    103         return mCategories;
    104     }
    105 
    106     public List<Condition> getConditions() {
    107         return mConditions;
    108     }
    109 
    110     public List<Tile> getSuggestions() {
    111         return mSuggestions;
    112     }
    113 
    114     public int getSuggestionMode() {
    115         return mSuggestionMode;
    116     }
    117 
    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      */
    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      */
    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      */
    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 
    186     public boolean hasMoreSuggestions() {
    187         return mSuggestionMode == SUGGESTION_MODE_COLLAPSED
    188                 || (mSuggestionMode == SUGGESTION_MODE_DEFAULT
    189                 && mSuggestions.size() > DEFAULT_SUGGESTION_COUNT);
    190     }
    191 
    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      */
    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      */
    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      */
    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 }