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.Drawable; 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; 24 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; 29 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; 35 36 /** 37 * Description about data list used in the DashboardAdapter. In the data list each item can be 38 * Condition, suggestion or category tile. 39 * <p> 40 * ItemsData has inner class Item, which represents the Item in data list. 41 */ 42 public class DashboardData { 43 public static final int POSITION_NOT_FOUND = -1; 44 public static final int MAX_SUGGESTION_COUNT = 2; 45 46 // stable id for different type of items. 47 @VisibleForTesting 48 static final int STABLE_ID_SUGGESTION_CONTAINER = 0; 49 static final int STABLE_ID_SUGGESTION_CONDITION_DIVIDER = 1; 50 @VisibleForTesting 51 static final int STABLE_ID_CONDITION_HEADER = 2; 52 @VisibleForTesting 53 static final int STABLE_ID_CONDITION_FOOTER = 3; 54 @VisibleForTesting 55 static final int STABLE_ID_CONDITION_CONTAINER = 4; 56 57 private final List<Item> mItems; 58 private final DashboardCategory mCategory; 59 private final List<Condition> mConditions; 60 private final List<Suggestion> mSuggestions; 61 private final boolean mConditionExpanded; 62 DashboardData(Builder builder)63 private DashboardData(Builder builder) { 64 mCategory = builder.mCategory; 65 mConditions = builder.mConditions; 66 mSuggestions = builder.mSuggestions; 67 mConditionExpanded = builder.mConditionExpanded; 68 mItems = new ArrayList<>(); 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 getCategory()102 public DashboardCategory getCategory() { 103 return mCategory; 104 } 105 getConditions()106 public List<Condition> getConditions() { 107 return mConditions; 108 } 109 getSuggestions()110 public List<Suggestion> getSuggestions() { 111 return mSuggestions; 112 } 113 hasSuggestion()114 public boolean hasSuggestion() { 115 return sizeOf(mSuggestions) > 0; 116 } 117 isConditionExpanded()118 public boolean isConditionExpanded() { 119 return mConditionExpanded; 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 * Add item into list when {@paramref add} is true. 167 * 168 * @param item maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null 169 * @param type type of the item, and value is the layout id 170 * @param stableId The stable id for this item 171 * @param add flag about whether to add item into list 172 */ addToItemList(Object item, int type, int stableId, boolean add)173 private void addToItemList(Object item, int type, int stableId, boolean add) { 174 if (add) { 175 mItems.add(new Item(item, type, stableId)); 176 } 177 } 178 179 /** 180 * Build the mItems list using mConditions, mSuggestions, mCategories data 181 * and mIsShowingAll, mConditionExpanded flag. 182 */ buildItemsData()183 private void buildItemsData() { 184 final List<Condition> conditions = getConditionsToShow(mConditions); 185 final boolean hasConditions = sizeOf(conditions) > 0; 186 187 final List<Suggestion> suggestions = getSuggestionsToShow(mSuggestions); 188 final boolean hasSuggestions = sizeOf(suggestions) > 0; 189 190 /* Suggestion container. This is the card view that contains the list of suggestions. 191 * This will be added whenever the suggestion list is not empty */ 192 addToItemList(suggestions, R.layout.suggestion_container, 193 STABLE_ID_SUGGESTION_CONTAINER, hasSuggestions); 194 195 /* Divider between suggestion and conditions if both are present. */ 196 addToItemList(null /* item */, R.layout.horizontal_divider, 197 STABLE_ID_SUGGESTION_CONDITION_DIVIDER, hasSuggestions && hasConditions); 198 199 /* Condition header. This will be present when there is condition and it is collapsed */ 200 addToItemList(new ConditionHeaderData(conditions), 201 R.layout.condition_header, 202 STABLE_ID_CONDITION_HEADER, hasConditions && !mConditionExpanded); 203 204 /* Condition container. This is the card view that contains the list of conditions. 205 * This will be added whenever the condition list is not empty and expanded */ 206 addToItemList(conditions, R.layout.condition_container, 207 STABLE_ID_CONDITION_CONTAINER, hasConditions && mConditionExpanded); 208 209 /* Condition footer. This will be present when there is condition and it is expanded */ 210 addToItemList(null /* item */, R.layout.condition_footer, 211 STABLE_ID_CONDITION_FOOTER, hasConditions && mConditionExpanded); 212 213 if (mCategory != null) { 214 final List<Tile> tiles = mCategory.getTiles(); 215 for (int i = 0; i < tiles.size(); i++) { 216 final Tile tile = tiles.get(i); 217 addToItemList(tile, R.layout.dashboard_tile, Objects.hash(tile.title), 218 true /* add */); 219 } 220 } 221 } 222 sizeOf(List<?> list)223 private static int sizeOf(List<?> list) { 224 return list == null ? 0 : list.size(); 225 } 226 getConditionsToShow(List<Condition> conditions)227 private List<Condition> getConditionsToShow(List<Condition> conditions) { 228 if (conditions == null) { 229 return null; 230 } 231 List<Condition> result = new ArrayList<>(); 232 final int size = conditions == null ? 0 : conditions.size(); 233 for (int i = 0; i < size; i++) { 234 final Condition condition = conditions.get(i); 235 if (condition.shouldShow()) { 236 result.add(condition); 237 } 238 } 239 return result; 240 } 241 getSuggestionsToShow(List<Suggestion> suggestions)242 private List<Suggestion> getSuggestionsToShow(List<Suggestion> suggestions) { 243 if (suggestions == null) { 244 return null; 245 } 246 if (suggestions.size() <= MAX_SUGGESTION_COUNT) { 247 return suggestions; 248 } 249 final List<Suggestion> suggestionsToShow = new ArrayList<>(MAX_SUGGESTION_COUNT); 250 for (int i = 0; i < MAX_SUGGESTION_COUNT; i++) { 251 suggestionsToShow.add(suggestions.get(i)); 252 } 253 return suggestionsToShow; 254 } 255 256 /** 257 * Builder used to build the ItemsData 258 */ 259 public static class Builder { 260 private DashboardCategory mCategory; 261 private List<Condition> mConditions; 262 private List<Suggestion> mSuggestions; 263 private boolean mConditionExpanded; 264 Builder()265 public Builder() { 266 } 267 Builder(DashboardData dashboardData)268 public Builder(DashboardData dashboardData) { 269 mCategory = dashboardData.mCategory; 270 mConditions = dashboardData.mConditions; 271 mSuggestions = dashboardData.mSuggestions; 272 mConditionExpanded = dashboardData.mConditionExpanded; 273 } 274 setCategory(DashboardCategory category)275 public Builder setCategory(DashboardCategory category) { 276 this.mCategory = category; 277 return this; 278 } 279 setConditions(List<Condition> conditions)280 public Builder setConditions(List<Condition> conditions) { 281 this.mConditions = conditions; 282 return this; 283 } 284 setSuggestions(List<Suggestion> suggestions)285 public Builder setSuggestions(List<Suggestion> suggestions) { 286 this.mSuggestions = suggestions; 287 return this; 288 } 289 setConditionExpanded(boolean expanded)290 public Builder setConditionExpanded(boolean expanded) { 291 this.mConditionExpanded = expanded; 292 return this; 293 } 294 build()295 public DashboardData build() { 296 return new DashboardData(this); 297 } 298 } 299 300 /** 301 * A DiffCallback to calculate the difference between old and new Item 302 * List in DashboardData 303 */ 304 public static class ItemsDataDiffCallback extends DiffUtil.Callback { 305 final private List<Item> mOldItems; 306 final private List<Item> mNewItems; 307 ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems)308 public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) { 309 mOldItems = oldItems; 310 mNewItems = newItems; 311 } 312 313 @Override getOldListSize()314 public int getOldListSize() { 315 return mOldItems.size(); 316 } 317 318 @Override getNewListSize()319 public int getNewListSize() { 320 return mNewItems.size(); 321 } 322 323 @Override areItemsTheSame(int oldItemPosition, int newItemPosition)324 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 325 return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id; 326 } 327 328 @Override areContentsTheSame(int oldItemPosition, int newItemPosition)329 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 330 return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition)); 331 } 332 333 } 334 335 /** 336 * An item contains the data needed in the DashboardData. 337 */ 338 static class Item { 339 // valid types in field type 340 private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile; 341 private static final int TYPE_SUGGESTION_CONTAINER = 342 R.layout.suggestion_container; 343 private static final int TYPE_CONDITION_CONTAINER = 344 R.layout.condition_container; 345 private static final int TYPE_CONDITION_HEADER = 346 R.layout.condition_header; 347 private static final int TYPE_CONDITION_FOOTER = 348 R.layout.condition_footer; 349 private static final int TYPE_SUGGESTION_CONDITION_DIVIDER = R.layout.horizontal_divider; 350 351 @IntDef({TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_CONTAINER, TYPE_CONDITION_CONTAINER, 352 TYPE_CONDITION_HEADER, TYPE_CONDITION_FOOTER, TYPE_SUGGESTION_CONDITION_DIVIDER}) 353 @Retention(RetentionPolicy.SOURCE) 354 public @interface ItemTypes { 355 } 356 357 /** 358 * The main data object in item, usually is a {@link Tile}, {@link Condition} 359 * object. This object can also be null when the 360 * item is an divider line. Please refer to {@link #buildItemsData()} for 361 * detail usage of the Item. 362 */ 363 public final Object entity; 364 365 /** 366 * The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile) 367 */ 368 @ItemTypes 369 public final int type; 370 371 /** 372 * Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item. 373 */ 374 public final int id; 375 Item(Object entity, @ItemTypes int type, int id)376 public Item(Object entity, @ItemTypes int type, int id) { 377 this.entity = entity; 378 this.type = type; 379 this.id = id; 380 } 381 382 /** 383 * Override it to make comparision in the {@link ItemsDataDiffCallback} 384 * 385 * @param obj object to compared with 386 * @return true if the same object or has equal value. 387 */ 388 @Override equals(Object obj)389 public boolean equals(Object obj) { 390 if (this == obj) { 391 return true; 392 } 393 394 if (!(obj instanceof Item)) { 395 return false; 396 } 397 398 final Item targetItem = (Item) obj; 399 if (type != targetItem.type || id != targetItem.id) { 400 return false; 401 } 402 403 switch (type) { 404 case TYPE_DASHBOARD_TILE: 405 final Tile localTile = (Tile) entity; 406 final Tile targetTile = (Tile) targetItem.entity; 407 408 // Only check title and summary for dashboard tile 409 return TextUtils.equals(localTile.title, targetTile.title) 410 && TextUtils.equals(localTile.summary, targetTile.summary); 411 case TYPE_SUGGESTION_CONTAINER: 412 case TYPE_CONDITION_CONTAINER: 413 // If entity is suggestion and contains remote view, force refresh 414 final List entities = (List) entity; 415 if (!entities.isEmpty()) { 416 Object firstEntity = entities.get(0); 417 if (firstEntity instanceof Tile 418 && ((Tile) firstEntity).remoteViews != null) { 419 return false; 420 } 421 } 422 // Otherwise Fall through to default 423 default: 424 return entity == null ? targetItem.entity == null 425 : entity.equals(targetItem.entity); 426 } 427 } 428 } 429 430 /** 431 * This class contains the data needed to build the suggestion/condition header. The data can 432 * also be used to check the diff in DiffUtil.Callback 433 */ 434 public static class ConditionHeaderData { 435 public final List<Drawable> conditionIcons; 436 public final CharSequence title; 437 public final int conditionCount; 438 ConditionHeaderData(List<Condition> conditions)439 public ConditionHeaderData(List<Condition> conditions) { 440 conditionCount = sizeOf(conditions); 441 title = conditionCount > 0 ? conditions.get(0).getTitle() : null; 442 conditionIcons = new ArrayList<>(); 443 for (int i = 0; conditions != null && i < conditions.size(); i++) { 444 final Condition condition = conditions.get(i); 445 conditionIcons.add(condition.getIcon()); 446 } 447 } 448 } 449 450 } 451