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 }