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