1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.download.home.list;
6 
7 import androidx.annotation.IntDef;
8 
9 import org.chromium.chrome.browser.download.home.DownloadManagerUiConfig;
10 import org.chromium.chrome.browser.download.home.filter.Filters.FilterType;
11 import org.chromium.chrome.browser.download.home.list.ListItem.OfflineItemListItem;
12 import org.chromium.chrome.browser.download.home.list.ListItem.ViewListItem;
13 import org.chromium.components.browser_ui.util.date.CalendarUtils;
14 import org.chromium.components.offline_items_collection.LegacyHelpers;
15 import org.chromium.components.offline_items_collection.OfflineItem;
16 import org.chromium.components.offline_items_collection.OfflineItemFilter;
17 import org.chromium.components.offline_items_collection.OfflineItemState;
18 
19 import java.lang.annotation.Retention;
20 import java.lang.annotation.RetentionPolicy;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Date;
24 import java.util.List;
25 
26 /** Utility methods for representing {@link ListItem}s in a {@link RecyclerView} list. */
27 public class ListUtils {
28     /** The potential types of list items that could be displayed. */
29     @IntDef({ViewType.DATE, ViewType.IN_PROGRESS, ViewType.GENERIC, ViewType.VIDEO, ViewType.AUDIO,
30             ViewType.IMAGE, ViewType.IMAGE_FULL_WIDTH, ViewType.CUSTOM_VIEW,
31             ViewType.SECTION_HEADER, ViewType.IN_PROGRESS_VIDEO, ViewType.IN_PROGRESS_IMAGE,
32             ViewType.PREFETCH_ARTICLE, ViewType.GROUP_CARD_ITEM, ViewType.GROUP_CARD_HEADER,
33             ViewType.GROUP_CARD_FOOTER, ViewType.PAGINATION_HEADER, ViewType.GROUP_CARD_DIVIDER_TOP,
34             ViewType.GROUP_CARD_DIVIDER_MIDDLE, ViewType.GROUP_CARD_DIVIDER_BOTTOM})
35     @Retention(RetentionPolicy.SOURCE)
36     public @interface ViewType {
37         int DATE = 0;
38         int IN_PROGRESS = 1;
39         int GENERIC = 2;
40         int VIDEO = 3;
41         int AUDIO = 4;
42         int IMAGE = 5;
43         int IMAGE_FULL_WIDTH = 6;
44         int CUSTOM_VIEW = 7;
45         int SECTION_HEADER = 8;
46         int IN_PROGRESS_VIDEO = 9;
47         int IN_PROGRESS_IMAGE = 10;
48         int PREFETCH_ARTICLE = 11;
49         int GROUP_CARD_ITEM = 12;
50         int GROUP_CARD_HEADER = 13;
51         int GROUP_CARD_FOOTER = 14;
52         int GROUP_CARD_DIVIDER_TOP = 15;
53         int GROUP_CARD_DIVIDER_MIDDLE = 16;
54         int GROUP_CARD_DIVIDER_BOTTOM = 17;
55         int PAGINATION_HEADER = 18;
56     }
57 
58     /**
59      * A visual ordering of the {@link Filters#FilterType}s to determine what order the sections
60      * should appear in the UI.
61      *
62      * Note that this list should have an entry for each {@link Filters#FilterType} that can be
63      * shown visually and asserts will fire if it does not.
64      */
65     private static final int[] FILTER_TYPE_ORDER_LIST =
66             new int[] {FilterType.NONE, FilterType.VIDEOS, FilterType.MUSIC, FilterType.IMAGES,
67                     FilterType.SITES, FilterType.OTHER, FilterType.DOCUMENT, FilterType.PREFETCHED};
68 
69     /** Converts a given list of {@link ListItem}s to a list of {@link OfflineItem}s. */
toOfflineItems(Collection<ListItem> items)70     public static List<OfflineItem> toOfflineItems(Collection<ListItem> items) {
71         List<OfflineItem> offlineItems = new ArrayList<>();
72         for (ListItem item : items) {
73             if (item instanceof ListItem.OfflineItemListItem) {
74                 offlineItems.add(((ListItem.OfflineItemListItem) item).item);
75             }
76         }
77         return offlineItems;
78     }
79 
80     /**
81      * Analyzes a {@link ListItem} and finds the most appropriate {@link ViewType} based on the
82      * current state.
83      * @param item   The {@link ListItem} to determine the {@link ViewType} for.
84      * @param config The {@link DownloadManagerUiConfig}.
85      * @return       The type of {@link ViewType} to use for a particular {@link ListItem}.
86      * @see          ViewType
87      */
getViewTypeForItem(ListItem item, DownloadManagerUiConfig config)88     public static @ViewType int getViewTypeForItem(ListItem item, DownloadManagerUiConfig config) {
89         if (item instanceof ViewListItem) return ViewType.CUSTOM_VIEW;
90         if (item instanceof ListItem.SectionHeaderListItem) return ViewType.SECTION_HEADER;
91         if (item instanceof ListItem.PaginationListItem) return ViewType.PAGINATION_HEADER;
92         if (item instanceof ListItem.CardHeaderListItem) {
93             return ViewType.GROUP_CARD_HEADER;
94         }
95         if (item instanceof ListItem.CardFooterListItem) {
96             return ViewType.GROUP_CARD_FOOTER;
97         }
98 
99         if (item instanceof ListItem.CardDividerListItem) {
100             switch (((ListItem.CardDividerListItem) item).position) {
101                 case TOP:
102                     return ViewType.GROUP_CARD_DIVIDER_TOP;
103                 case MIDDLE:
104                     return ViewType.GROUP_CARD_DIVIDER_MIDDLE;
105                 case BOTTOM:
106                     return ViewType.GROUP_CARD_DIVIDER_BOTTOM;
107             }
108         }
109 
110         if (item instanceof OfflineItemListItem) {
111             OfflineItemListItem offlineItem = (OfflineItemListItem) item;
112             if (offlineItem.isGrouped) return ViewType.GROUP_CARD_ITEM;
113             if (offlineItem.item.schedule != null) return ViewType.GENERIC;
114 
115             boolean inProgress = offlineItem.item.state == OfflineItemState.IN_PROGRESS
116                     || offlineItem.item.state == OfflineItemState.PAUSED
117                     || offlineItem.item.state == OfflineItemState.INTERRUPTED
118                     || offlineItem.item.state == OfflineItemState.PENDING
119                     || offlineItem.item.state == OfflineItemState.FAILED;
120 
121             if (config.useGenericViewTypes) {
122                 return inProgress ? ViewType.IN_PROGRESS : ViewType.GENERIC;
123             }
124 
125             if (offlineItem.item.isSuggested) {
126                 if (offlineItem.item.filter == OfflineItemFilter.PAGE) {
127                     return ViewType.PREFETCH_ARTICLE;
128                 } else if (offlineItem.item.filter == OfflineItemFilter.AUDIO) {
129                     return ViewType.AUDIO;
130                 }
131             }
132 
133             switch (offlineItem.item.filter) {
134                 case OfflineItemFilter.VIDEO:
135                     return inProgress ? ViewType.IN_PROGRESS_VIDEO : ViewType.VIDEO;
136                 case OfflineItemFilter.IMAGE:
137                     return inProgress ? ViewType.IN_PROGRESS_IMAGE
138                                       : (offlineItem.spanFullWidth ? ViewType.IMAGE_FULL_WIDTH
139                                                                    : ViewType.IMAGE);
140                 // case OfflineItemFilter.PAGE:
141                 // case OfflineItemFilter.AUDIO:
142                 // case OfflineItemFilter.OTHER:
143                 // case OfflineItemFilter.DOCUMENT:
144                 default:
145                     return inProgress ? ViewType.IN_PROGRESS : ViewType.GENERIC;
146             }
147         }
148 
149         assert false;
150         return ViewType.GENERIC;
151     }
152 
153     /** @return Whether the given {@link ListItem} can be grouped inside a card. */
canGroup(ListItem listItem)154     public static boolean canGroup(ListItem listItem) {
155         if (!(listItem instanceof OfflineItemListItem)) return false;
156         return LegacyHelpers.isLegacyContentIndexedItem(((OfflineItemListItem) listItem).item.id);
157     }
158 
159     /**
160      * Analyzes a {@link ListItem} and finds the best span size based on the current state.  Span
161      * size determines how many columns this {@link ListItem}'s {@link View} will take up in the
162      * overall list.
163      * @param item      The {@link ListItem} to determine the span size for.
164      * @param config    The {@link DownloadManagerUiConfig}.
165      * @param spanCount The maximum span amount of columns {@code item} can take up.
166      * @return          The number of columns {@code item} should take.
167      * @see             GridLayoutManager.SpanSizeLookup
168      */
getSpanSize(ListItem item, DownloadManagerUiConfig config, int spanCount)169     public static int getSpanSize(ListItem item, DownloadManagerUiConfig config, int spanCount) {
170         switch (getViewTypeForItem(item, config)) {
171             case ViewType.IMAGE: // Intentional fallthrough.
172             case ViewType.IN_PROGRESS_IMAGE:
173                 return 1;
174             default:
175                 return spanCount;
176         }
177     }
178 
179     /**
180      * Helper method to determine which item type section to show first in the list.
181      * @return -1 if {@code a} should be shown before {@code b}.
182      *          0 if {@code a} == {@code b}.
183      *          1 if {@code a} should be shown after {@code b}.
184      */
compareFilterTypesTo(@ilterType int a, @FilterType int b)185     public static int compareFilterTypesTo(@FilterType int a, @FilterType int b) {
186         int aPriority = getVisualPriorityForFilter(a);
187         int bPriority = getVisualPriorityForFilter(b);
188         return (aPriority < bPriority) ? -1 : ((aPriority == bPriority) ? 0 : 1);
189     }
190 
191     /**
192      * Helper method to compare list items based on date. Two items have equal if they both got
193      * created on the same day.
194      * @return -1 if {@code a} should be shown before {@code b}.
195      *          0 if {@code a} == {@code b}.
196      *          1 if {@code a} should be shown after {@code b}.
197      */
compareItemByDate(OfflineItem a, OfflineItem b)198     public static int compareItemByDate(OfflineItem a, OfflineItem b) {
199         Date aDay = CalendarUtils.getStartOfDay(a.creationTimeMs).getTime();
200         Date bDay = CalendarUtils.getStartOfDay(b.creationTimeMs).getTime();
201         return bDay.compareTo(aDay);
202     }
203 
204     /**
205      * Helper method to compare list items based on timestamp.
206      * @return -1 if {@code a} should be shown before {@code b}.
207      *          0 if {@code a} == {@code b}.
208      *          1 if {@code a} should be shown after {@code b}.
209      */
compareItemByTimestamp(OfflineItem a, OfflineItem b)210     public static int compareItemByTimestamp(OfflineItem a, OfflineItem b) {
211         return Long.compare(b.creationTimeMs, a.creationTimeMs);
212     }
213 
214     /**
215      * Helper method to compare list items based on ID.
216      * @return -1 if {@code a} should be shown before {@code b}.
217      *          0 if {@code a} == {@code b}.
218      *          1 if {@code a} should be shown after {@code b}.
219      */
compareItemByID(OfflineItem a, OfflineItem b)220     public static int compareItemByID(OfflineItem a, OfflineItem b) {
221         int comparison = a.id.namespace.compareTo(b.id.namespace);
222         if (comparison != 0) return comparison;
223         return a.id.id.compareTo(b.id.id);
224     }
225     /**
226      * Helper method to compare list items based on {@link OfflineItemSchedule}.
227      * @return -1 if {@code a} should be shown before {@code b}.
228      *          0 if {@code a} == {@code b}.
229      *          1 if {@code a} should be shown after {@code b}.
230      */
compareItemBySchedule(OfflineItem a, OfflineItem b)231     public static int compareItemBySchedule(OfflineItem a, OfflineItem b) {
232         if (a.schedule != null && b.schedule != null) {
233             return a.schedule.startTimeMs <= b.schedule.startTimeMs ? -1 : 1;
234         }
235 
236         if (a.schedule == null && b.schedule == null) return 0;
237         return a.schedule != null ? -1 : 1;
238     }
239 
getVisualPriorityForFilter(@ilterType int type)240     private static int getVisualPriorityForFilter(@FilterType int type) {
241         for (int i = 0; i < FILTER_TYPE_ORDER_LIST.length; i++) {
242             if (FILTER_TYPE_ORDER_LIST[i] == type) return i;
243         }
244 
245         assert false
246             : "Unexpected Filters.FilterType (did you forget to update FILTER_TYPE_ORDER_LIST?).";
247         return 0;
248     }
249 }
250