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 android.content.Context;
8 import android.content.res.Resources;
9 import android.text.format.DateUtils;
10 import android.text.format.Formatter;
11 
12 import androidx.annotation.DrawableRes;
13 
14 import org.chromium.base.ContextUtils;
15 import org.chromium.base.MathUtils;
16 import org.chromium.chrome.R;
17 import org.chromium.chrome.browser.download.StringUtils;
18 import org.chromium.chrome.browser.download.home.filter.Filters;
19 import org.chromium.chrome.browser.download.home.list.view.CircularProgressView;
20 import org.chromium.chrome.browser.download.home.list.view.CircularProgressView.UiState;
21 import org.chromium.components.browser_ui.util.date.CalendarFactory;
22 import org.chromium.components.browser_ui.util.date.CalendarUtils;
23 import org.chromium.components.offline_items_collection.LegacyHelpers;
24 import org.chromium.components.offline_items_collection.OfflineItem;
25 import org.chromium.components.offline_items_collection.OfflineItem.Progress;
26 import org.chromium.components.offline_items_collection.OfflineItemFilter;
27 import org.chromium.components.offline_items_collection.OfflineItemProgressUnit;
28 import org.chromium.components.offline_items_collection.OfflineItemState;
29 import org.chromium.components.url_formatter.SchemeDisplay;
30 import org.chromium.components.url_formatter.UrlFormatter;
31 
32 import java.util.Calendar;
33 import java.util.Date;
34 
35 /** A set of helper utility methods for the UI. */
36 public final class UiUtils {
UiUtils()37     private UiUtils() {}
38 
39     /**
40      * Builds the accessibility text to be used for a given chip on the chips row.
41      * @param resources The resources to use for lookup.
42      * @param filter The filter type of the chip.
43      * @param itemCount The number of items being shown on the given chip.
44      * @return The content description to be used for the chip.
45      */
getChipContentDescription( Resources resources, @Filters.FilterType int filter, int itemCount)46     public static String getChipContentDescription(
47             Resources resources, @Filters.FilterType int filter, int itemCount) {
48         switch (filter) {
49             case Filters.FilterType.NONE:
50                 return resources.getQuantityString(
51                         R.plurals.accessibility_download_manager_ui_generic, itemCount, itemCount);
52             case Filters.FilterType.VIDEOS:
53                 return resources.getQuantityString(
54                         R.plurals.accessibility_download_manager_ui_video, itemCount, itemCount);
55             case Filters.FilterType.MUSIC:
56                 return resources.getQuantityString(
57                         R.plurals.accessibility_download_manager_ui_audio, itemCount, itemCount);
58             case Filters.FilterType.IMAGES:
59                 return resources.getQuantityString(
60                         R.plurals.accessibility_download_manager_ui_images, itemCount, itemCount);
61             case Filters.FilterType.SITES:
62                 return resources.getQuantityString(
63                         R.plurals.accessibility_download_manager_ui_pages, itemCount, itemCount);
64             case Filters.FilterType.OTHER:
65                 return resources.getQuantityString(
66                         R.plurals.accessibility_download_manager_ui_generic, itemCount, itemCount);
67             default:
68                 assert false;
69                 return null;
70         }
71     }
72 
73     /**
74      * Converts {@code date} to a string meant to be used as a prefetched item timestamp.
75      * @param date The {@link Date} to convert.
76      * @return     The {@link CharSequence} representing the timestamp.
77      */
generatePrefetchTimestamp(Date date)78     public static CharSequence generatePrefetchTimestamp(Date date) {
79         Context context = ContextUtils.getApplicationContext();
80 
81         Calendar calendar1 = CalendarFactory.get();
82         Calendar calendar2 = CalendarFactory.get();
83 
84         calendar1.setTimeInMillis(System.currentTimeMillis());
85         calendar2.setTime(date);
86 
87         if (CalendarUtils.isSameDay(calendar1, calendar2)) {
88             int hours = (int) MathUtils.clamp(
89                     (calendar1.getTimeInMillis() - calendar2.getTimeInMillis())
90                             / DateUtils.HOUR_IN_MILLIS,
91                     1, 23);
92             return context.getResources().getQuantityString(
93                     R.plurals.download_manager_n_hours, hours, hours);
94         } else {
95             return DateUtils.formatDateTime(context, date.getTime(), DateUtils.FORMAT_SHOW_YEAR);
96         }
97     }
98 
99     /**
100      * Generates a caption for a prefetched item.
101      * @param item The {@link OfflineItem} to generate a caption for.
102      * @return     The {@link CharSequence} representing the caption.
103      */
generatePrefetchCaption(OfflineItem item)104     public static CharSequence generatePrefetchCaption(OfflineItem item) {
105         Context context = ContextUtils.getApplicationContext();
106         String displaySize = Formatter.formatFileSize(context, item.totalSizeBytes);
107         String displayUrl = UrlFormatter.formatUrlForSecurityDisplay(
108                 item.originalUrl, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
109         return context.getString(
110                 R.string.download_manager_prefetch_caption, displayUrl, displaySize);
111     }
112 
113     /**
114      * Generates a caption for a generic item.
115      * @param item The {@link OfflineItem} to generate a caption for.
116      * @return     The {@link CharSequence} representing the caption.
117      */
generateGenericCaption(OfflineItem item)118     public static CharSequence generateGenericCaption(OfflineItem item) {
119         Context context = ContextUtils.getApplicationContext();
120         String displayUrl = UrlFormatter.formatUrlForSecurityDisplay(
121                 item.pageUrl, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
122 
123         if (item.totalSizeBytes == 0) {
124             return context.getString(
125                     R.string.download_manager_list_item_description_no_size, displayUrl);
126         }
127 
128         String displaySize = Formatter.formatFileSize(context, item.totalSizeBytes);
129         return context.getString(
130                 R.string.download_manager_list_item_description, displaySize, displayUrl);
131     }
132 
133     /** @return Whether or not {@code item} can show a thumbnail in the UI. */
canHaveThumbnails(OfflineItem item)134     public static boolean canHaveThumbnails(OfflineItem item) {
135         switch (item.filter) {
136             case OfflineItemFilter.PAGE:
137             case OfflineItemFilter.VIDEO:
138             case OfflineItemFilter.IMAGE:
139             case OfflineItemFilter.AUDIO:
140                 return true;
141             default:
142                 return false;
143         }
144     }
145 
146     /** @return A drawable resource id representing an icon for {@code item}. */
getIconForItem(OfflineItem item)147     public static @DrawableRes int getIconForItem(OfflineItem item) {
148         switch (Filters.fromOfflineItem(item)) {
149             case Filters.FilterType.NONE:
150                 return R.drawable.ic_file_download_24dp;
151             case Filters.FilterType.SITES:
152                 return R.drawable.ic_globe_24dp;
153             case Filters.FilterType.VIDEOS:
154                 return R.drawable.ic_videocam_24dp;
155             case Filters.FilterType.MUSIC:
156                 return R.drawable.ic_music_note_24dp;
157             case Filters.FilterType.IMAGES:
158                 return R.drawable.ic_drive_image_24dp;
159             case Filters.FilterType.DOCUMENT:
160                 return R.drawable.ic_drive_document_24dp;
161             case Filters.FilterType.OTHER: // Intentional fallthrough.
162             default:
163                 return R.drawable.ic_drive_file_24dp;
164         }
165     }
166 
167     /**
168      * @return A drawable resource id representing the small media icon to be shown on prefetch
169      *         cards.
170      */
getMediaPlayIconForPrefetchCards(OfflineItem item)171     public static @DrawableRes int getMediaPlayIconForPrefetchCards(OfflineItem item) {
172         switch (item.filter) {
173             case OfflineItemFilter.VIDEO: // fallthrough
174             case OfflineItemFilter.AUDIO:
175                 // TODO(shaktisahu): Provide vector icon for audio.
176                 return R.drawable.ic_play_circle_filled_24dp;
177             default:
178                 return 0;
179         }
180     }
181 
182     /**
183      * Generates a caption for downloads that are in-progress.
184      * @param item       The {@link OfflineItem} to generate a caption for.
185      * @param abbreviate Whether or not to abbreviate the caption for smaller UI surfaces.
186      * @return           The {@link CharSequence} representing the caption.
187      */
generateInProgressCaption(OfflineItem item, boolean abbreviate)188     public static CharSequence generateInProgressCaption(OfflineItem item, boolean abbreviate) {
189         return abbreviate ? generateInProgressShortCaption(item)
190                           : generateInProgressLongCaption(item);
191     }
192 
193     /**
194      * Populates a {@link CircularProgressView} based on the contents of an {@link OfflineItem}.
195      * This is a helper glue method meant to consolidate the setting of {@link CircularProgressView}
196      * state.
197      * @param view The {@link CircularProgressView} to update.
198      * @param item The {@link OfflineItem} to use as the source of the update state.
199      */
setProgressForOfflineItem(CircularProgressView view, OfflineItem item)200     public static void setProgressForOfflineItem(CircularProgressView view, OfflineItem item) {
201         Progress progress = item.progress;
202         final boolean indeterminate = progress != null && progress.isIndeterminate();
203         final int determinateProgress =
204                 progress != null && !indeterminate ? progress.getPercentage() : 0;
205         final int activeProgress =
206                 indeterminate ? CircularProgressView.INDETERMINATE : determinateProgress;
207         final int inactiveProgress = indeterminate ? 0 : determinateProgress;
208 
209         @UiState
210         int shownState;
211         int shownProgress;
212 
213         switch (item.state) {
214             case OfflineItemState.PENDING: // Intentional fallthrough.
215             case OfflineItemState.IN_PROGRESS:
216                 shownState = CircularProgressView.UiState.RUNNING;
217                 break;
218             case OfflineItemState.FAILED: // Intentional fallthrough.
219             case OfflineItemState.CANCELLED:
220                 shownState = CircularProgressView.UiState.RETRY;
221                 break;
222             case OfflineItemState.PAUSED:
223                 shownState = CircularProgressView.UiState.PAUSED;
224                 break;
225             case OfflineItemState.INTERRUPTED:
226                 shownState = item.isResumable ? CircularProgressView.UiState.RUNNING
227                                               : CircularProgressView.UiState.RETRY;
228                 break;
229             case OfflineItemState.COMPLETE: // Intentional fallthrough.
230             default:
231                 assert false : "Unexpected state for progress bar.";
232                 shownState = CircularProgressView.UiState.RETRY;
233                 break;
234         }
235 
236         switch (item.state) {
237             case OfflineItemState.PAUSED: // Intentional fallthrough.
238             case OfflineItemState.PENDING:
239                 shownProgress = inactiveProgress;
240                 break;
241             case OfflineItemState.IN_PROGRESS:
242                 shownProgress = activeProgress;
243                 break;
244             case OfflineItemState.FAILED: // Intentional fallthrough.
245             case OfflineItemState.CANCELLED:
246                 shownProgress = 0;
247                 break;
248             case OfflineItemState.INTERRUPTED:
249                 shownProgress = item.isResumable ? inactiveProgress : 0;
250                 break;
251             case OfflineItemState.COMPLETE: // Intentional fallthrough.
252             default:
253                 assert false : "Unexpected state for progress bar.";
254                 shownProgress = 0;
255                 break;
256         }
257 
258         // TODO(dtrainor): This will need to be updated once we nail down failure cases
259         // (specifically non-retriable failures).
260         view.setState(shownState);
261         view.setProgress(shownProgress);
262     }
263 
264     /**
265      * Generates a detailed caption for downloads that are in-progress.
266      * @param item The {@link OfflineItem} to generate a caption for.
267      * @return     The {@link CharSequence} representing the caption.
268      */
generateInProgressLongCaption(OfflineItem item)269     private static CharSequence generateInProgressLongCaption(OfflineItem item) {
270         Context context = ContextUtils.getApplicationContext();
271         assert item.state != OfflineItemState.COMPLETE;
272 
273         OfflineItem.Progress progress = item.progress;
274 
275         // Make sure we have a valid OfflineItem.Progress to parse even if it's just for the failed
276         // message.
277         if (progress == null) {
278             if (item.totalSizeBytes > 0) {
279                 progress = new OfflineItem.Progress(
280                         0, item.totalSizeBytes, OfflineItemProgressUnit.BYTES);
281             } else {
282                 progress = new OfflineItem.Progress(0, 100L, OfflineItemProgressUnit.PERCENTAGE);
283             }
284         }
285 
286         CharSequence progressString = StringUtils.getProgressTextForUi(progress);
287         CharSequence statusString = null;
288 
289         switch (item.state) {
290             case OfflineItemState.PENDING:
291                 // TODO(crbug.com/891421): Add detailed pending state string from
292                 // StringUtils.getPendingStatusForUi().
293                 statusString = context.getString(R.string.download_manager_pending);
294                 break;
295             case OfflineItemState.IN_PROGRESS:
296                 if (item.timeRemainingMs > 0) {
297                     statusString = StringUtils.timeLeftForUi(context, item.timeRemainingMs);
298                 }
299                 break;
300             case OfflineItemState.FAILED: // Intentional fallthrough.
301             case OfflineItemState.CANCELLED: // Intentional fallthrough.
302             case OfflineItemState.INTERRUPTED:
303                 // TODO(crbug.com/891421): Add detailed failure state string from
304                 // StringUtils.getFailStatusForUi().
305                 statusString = context.getString(R.string.download_manager_failed);
306                 break;
307             case OfflineItemState.PAUSED:
308                 statusString = context.getString(R.string.download_manager_paused);
309                 break;
310             case OfflineItemState.COMPLETE: // Intentional fallthrough.
311             default:
312                 assert false;
313         }
314 
315         if (statusString == null) return progressString;
316 
317         return context.getString(
318                 R.string.download_manager_in_progress_description, progressString, statusString);
319     }
320 
321     /**
322      * Generates a short caption for downloads that are in-progress.
323      * @param item The {@link OfflineItem} to generate a short caption for.
324      * @return     The {@link CharSequence} representing the caption.
325      */
generateInProgressShortCaption(OfflineItem item)326     private static CharSequence generateInProgressShortCaption(OfflineItem item) {
327         Context context = ContextUtils.getApplicationContext();
328 
329         switch (item.state) {
330             case OfflineItemState.PENDING:
331                 return context.getString(R.string.download_manager_pending);
332             case OfflineItemState.IN_PROGRESS:
333                 if (item.timeRemainingMs > 0) {
334                     return StringUtils.timeLeftForUi(context, item.timeRemainingMs);
335                 } else {
336                     return StringUtils.getProgressTextForUi(item.progress);
337                 }
338             case OfflineItemState.FAILED: // Intentional fallthrough.
339             case OfflineItemState.CANCELLED: // Intentional fallthrough.
340             case OfflineItemState.INTERRUPTED:
341                 return context.getString(R.string.download_manager_failed);
342             case OfflineItemState.PAUSED:
343                 return context.getString(R.string.download_manager_paused);
344             case OfflineItemState.COMPLETE: // Intentional fallthrough.
345             default:
346                 assert false;
347                 return "";
348         }
349     }
350 
351     /** @return Whether the given {@link OfflineItem} can be shared. */
canShare(OfflineItem item)352     public static boolean canShare(OfflineItem item) {
353         return (item.state == OfflineItemState.COMPLETE)
354                 && (LegacyHelpers.isLegacyDownload(item.id)
355                         || LegacyHelpers.isLegacyOfflinePage(item.id));
356     }
357 
358     /** @return The domain associated with the given {@link OfflineItem}. */
getDomainForItem(OfflineItem offlineItem)359     public static String getDomainForItem(OfflineItem offlineItem) {
360         String formattedUrl = UrlFormatter.formatUrlForSecurityDisplay(
361                 offlineItem.pageUrl, SchemeDisplay.OMIT_HTTP_AND_HTTPS);
362         return formattedUrl;
363     }
364 }
365