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