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 }