| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chrome.browser.download; |
| |
| import android.app.Activity; |
| import android.content.ActivityNotFoundException; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.ColorStateList; |
| import android.net.Uri; |
| import android.os.Environment; |
| import android.os.StrictMode; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.Nullable; |
| import android.support.v7.content.res.AppCompatResources; |
| import android.text.TextUtils; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.Callback; |
| import org.chromium.base.ContentUriUtils; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.FileUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.StrictModeContext; |
| import org.chromium.base.TimeUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.library_loader.LibraryProcessType; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.ChromeTabbedActivity; |
| import org.chromium.chrome.browser.FileProviderHelper; |
| import org.chromium.chrome.browser.IntentHandler; |
| import org.chromium.chrome.browser.UrlConstants; |
| import org.chromium.chrome.browser.download.home.metrics.FileExtensions; |
| import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory; |
| import org.chromium.chrome.browser.download.ui.DownloadFilter; |
| import org.chromium.chrome.browser.download.ui.DownloadHistoryItemWrapper; |
| import org.chromium.chrome.browser.download.ui.DownloadHistoryItemWrapper.OfflineItemWrapper; |
| import org.chromium.chrome.browser.feature_engagement.ScreenshotTabObserver; |
| import org.chromium.chrome.browser.feature_engagement.TrackerFactory; |
| import org.chromium.chrome.browser.media.MediaViewerUtils; |
| import org.chromium.chrome.browser.offlinepages.DownloadUiActionFlags; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageOrigin; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; |
| import org.chromium.chrome.browser.offlinepages.downloads.OfflinePageDownloadBridge; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tabmodel.TabLaunchType; |
| import org.chromium.chrome.browser.tabmodel.document.TabDelegate; |
| import org.chromium.chrome.browser.util.ConversionUtils; |
| import org.chromium.chrome.browser.util.IntentUtils; |
| import org.chromium.components.download.DownloadState; |
| import org.chromium.components.download.ResumeMode; |
| import org.chromium.components.feature_engagement.EventConstants; |
| import org.chromium.components.feature_engagement.Tracker; |
| import org.chromium.components.offline_items_collection.ContentId; |
| import org.chromium.components.offline_items_collection.FailState; |
| import org.chromium.components.offline_items_collection.LaunchLocation; |
| import org.chromium.components.offline_items_collection.LegacyHelpers; |
| import org.chromium.components.offline_items_collection.OfflineItem; |
| import org.chromium.components.offline_items_collection.OfflineItem.Progress; |
| import org.chromium.components.offline_items_collection.OfflineItemProgressUnit; |
| import org.chromium.components.offline_items_collection.OfflineItemState; |
| import org.chromium.components.offline_items_collection.PendingState; |
| import org.chromium.components.offlinepages.SavePageResult; |
| import org.chromium.content_public.browser.BrowserStartupController; |
| import org.chromium.content_public.browser.LoadUrlParams; |
| import org.chromium.ui.base.DeviceFormFactor; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.io.File; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * A class containing some utility static methods. |
| */ |
| public class DownloadUtils { |
| |
| /** Strings indicating how many bytes have been downloaded for different units. */ |
| @VisibleForTesting |
| static final int[] BYTES_DOWNLOADED_STRINGS = { |
| R.string.file_size_downloaded_kb, |
| R.string.file_size_downloaded_mb, |
| R.string.file_size_downloaded_gb |
| }; |
| |
| private static final int[] BYTES_AVAILABLE_STRINGS = { |
| R.string.download_manager_ui_space_free_kb, R.string.download_manager_ui_space_free_mb, |
| R.string.download_manager_ui_space_free_gb}; |
| |
| private static final int[] BYTES_STRINGS = { |
| R.string.download_ui_kb, R.string.download_ui_mb, R.string.download_ui_gb}; |
| |
| private static final String TAG = "download"; |
| |
| private static final String DEFAULT_MIME_TYPE = "*/*"; |
| private static final String MIME_TYPE_DELIMITER = "/"; |
| private static final String MIME_TYPE_SHARING_URL = "text/plain"; |
| private static final String UNKNOWN_MIME_TYPE = "application/unknown"; |
| |
| private static final String EXTRA_IS_OFF_THE_RECORD = |
| "org.chromium.chrome.browser.download.IS_OFF_THE_RECORD"; |
| public static final String EXTRA_SHOW_PREFETCHED_CONTENT = |
| "org.chromium.chrome.browser.download.SHOW_PREFETCHED_CONTENT"; |
| |
| @VisibleForTesting |
| static final String ELLIPSIS = "\u2026"; |
| |
| /** |
| * Possible sizes of type-based icons. |
| */ |
| @IntDef({IconSize.DP_24, IconSize.DP_36}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface IconSize { |
| int DP_24 = 24; |
| int DP_36 = 36; |
| } |
| |
| /** |
| * Displays the download manager UI. Note the UI is different on tablets and on phones. |
| * @param activity The current activity is available. |
| * @param tab The current tab if it exists. |
| * @return Whether the UI was shown. |
| */ |
| public static boolean showDownloadManager(@Nullable Activity activity, @Nullable Tab tab) { |
| return showDownloadManager(activity, tab, false); |
| } |
| |
| /** |
| * Displays the download manager UI. Note the UI is different on tablets and on phones. |
| * @param activity The current activity is available. |
| * @param tab The current tab if it exists. |
| * @param showPrefetchedContent Whether the manager should start with prefetched content section |
| * expanded. |
| * @return Whether the UI was shown. |
| */ |
| public static boolean showDownloadManager( |
| @Nullable Activity activity, @Nullable Tab tab, boolean showPrefetchedContent) { |
| // Figure out what tab was last being viewed by the user. |
| if (activity == null) activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| Context appContext = ContextUtils.getApplicationContext(); |
| boolean isTablet; |
| |
| if (tab == null && activity instanceof ChromeTabbedActivity) { |
| ChromeTabbedActivity chromeActivity = ((ChromeTabbedActivity) activity); |
| tab = chromeActivity.getActivityTab(); |
| isTablet = chromeActivity.isTablet(); |
| } else { |
| Context displayContext = activity != null ? activity : appContext; |
| isTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(displayContext); |
| } |
| |
| if (isTablet) { |
| // Download Home shows up as a tab on tablets. |
| LoadUrlParams params = new LoadUrlParams(UrlConstants.DOWNLOADS_URL); |
| if (tab == null || !tab.isInitialized()) { |
| // Open a new tab, which pops Chrome into the foreground. |
| TabDelegate delegate = new TabDelegate(false); |
| delegate.createNewTab(params, TabLaunchType.FROM_CHROME_UI, null); |
| } else { |
| // Download Home shows up inside an existing tab, but only if the last Activity was |
| // the ChromeTabbedActivity. |
| tab.loadUrl(params); |
| |
| // Bring Chrome to the foreground, if possible. |
| Intent intent = IntentUtils.createBringTabToFrontIntent(tab.getId()); |
| if (intent != null) { |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| IntentUtils.safeStartActivity(appContext, intent); |
| } |
| } |
| } else { |
| // Download Home shows up as a new Activity on phones. |
| Intent intent = new Intent(); |
| intent.setClass(appContext, DownloadActivity.class); |
| intent.putExtra(EXTRA_SHOW_PREFETCHED_CONTENT, showPrefetchedContent); |
| if (tab != null) intent.putExtra(EXTRA_IS_OFF_THE_RECORD, tab.isIncognito()); |
| if (activity == null) { |
| // Stands alone in its own task. |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| appContext.startActivity(intent); |
| } else { |
| // Sits on top of another Activity. |
| intent.addFlags( |
| Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); |
| intent.putExtra(IntentHandler.EXTRA_PARENT_COMPONENT, activity.getComponentName()); |
| activity.startActivity(intent); |
| } |
| } |
| |
| if (BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted()) { |
| Profile profile = (tab == null ? Profile.getLastUsedProfile() : tab.getProfile()); |
| Tracker tracker = TrackerFactory.getTrackerForProfile(profile); |
| tracker.notifyEvent(EventConstants.DOWNLOAD_HOME_OPENED); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * @return Whether or not the Intent corresponds to a DownloadActivity that should show off the |
| * record downloads. |
| */ |
| public static boolean shouldShowOffTheRecordDownloads(Intent intent) { |
| return IntentUtils.safeGetBooleanExtra(intent, EXTRA_IS_OFF_THE_RECORD, false); |
| } |
| |
| /** |
| * @return Whether or not the prefetched content section should be expanded on launch of the |
| * DownloadActivity. |
| */ |
| public static boolean shouldShowPrefetchContent(Intent intent) { |
| return IntentUtils.safeGetBooleanExtra(intent, EXTRA_SHOW_PREFETCHED_CONTENT, false); |
| } |
| |
| /** |
| * Records metrics related to downloading a page. Should be called after a tap on the download |
| * page button. |
| * @param tab The Tab containing the page being downloaded. |
| */ |
| public static void recordDownloadPageMetrics(Tab tab) { |
| RecordHistogram.recordPercentageHistogram("OfflinePages.SavePage.PercentLoaded", |
| tab.getProgress()); |
| } |
| |
| /** |
| * Shows a "Downloading..." toast. Should be called after a download has been started. |
| * @param context The {@link Context} used to make the toast. |
| */ |
| public static void showDownloadStartToast(Context context) { |
| Toast.makeText(context, R.string.download_started, Toast.LENGTH_SHORT).show(); |
| } |
| |
| /** |
| * Issues a request to the {@link DownloadManagerService} associated to check for externally |
| * removed downloads. |
| * See {@link DownloadManagerService#checkForExternallyRemovedDownloads}. |
| * @param isOffTheRecord Whether to check downloads for the off the record profile. |
| */ |
| public static void checkForExternallyRemovedDownloads(boolean isOffTheRecord) { |
| if (isOffTheRecord) { |
| DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads( |
| true); |
| } |
| DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads( |
| false); |
| RecordUserAction.record( |
| "Android.DownloadManager.CheckForExternallyRemovedItems"); |
| } |
| |
| /** |
| * Trigger the download of an Offline Page. |
| * @param context Context to pull resources from. |
| */ |
| public static void downloadOfflinePage(Context context, Tab tab) { |
| OfflinePageOrigin origin = new OfflinePageOrigin(context, tab); |
| |
| if (tab.isShowingErrorPage()) { |
| // The download needs to be scheduled to happen at later time due to current network |
| // error. |
| final OfflinePageBridge bridge = OfflinePageBridge.getForProfile(tab.getProfile()); |
| bridge.scheduleDownload(tab.getWebContents(), OfflinePageBridge.ASYNC_NAMESPACE, |
| tab.getUrl(), DownloadUiActionFlags.PROMPT_DUPLICATE, origin); |
| } else { |
| // Otherwise, the download can be started immediately. |
| OfflinePageDownloadBridge.startDownload(tab, origin); |
| DownloadUtils.recordDownloadPageMetrics(tab); |
| } |
| |
| Tracker tracker = TrackerFactory.getTrackerForProfile(tab.getProfile()); |
| tracker.notifyEvent(EventConstants.DOWNLOAD_PAGE_STARTED); |
| } |
| |
| /** |
| * Whether the user should be allowed to download the current page. |
| * @param tab Tab displaying the page that will be downloaded. |
| * @return Whether the "Download Page" button should be enabled. |
| */ |
| public static boolean isAllowedToDownloadPage(Tab tab) { |
| if (tab == null) return false; |
| |
| // Offline pages isn't supported in Incognito. This should be checked before calling |
| // OfflinePageBridge.getForProfile because OfflinePageBridge instance will not be found |
| // for incognito profile. |
| if (tab.isIncognito()) return false; |
| |
| // Check if the page url is supported for saving. Only HTTP and HTTPS pages are allowed. |
| if (!OfflinePageBridge.canSavePage(tab.getUrl())) return false; |
| |
| // Download will only be allowed for the error page if download button is shown in the page. |
| if (tab.isShowingErrorPage()) { |
| final OfflinePageBridge bridge = OfflinePageBridge.getForProfile(tab.getProfile()); |
| return bridge.isShowingDownloadButtonInErrorPage(tab.getWebContents()); |
| } |
| |
| if (tab.isShowingInterstitialPage()) return false; |
| |
| // Don't allow re-downloading the currently displayed offline page. |
| if (OfflinePageUtils.isOfflinePage(tab)) return false; |
| |
| return true; |
| } |
| |
| /** |
| * Creates an Intent to share {@code items} with another app by firing an Intent to Android. |
| * |
| * Sharing a DownloadItem shares the file itself. Sharing an OfflinePageItem shares the archive |
| * file if the sharing is enabled. Otherwise, the URL is shared. |
| * |
| * @param items Items to share. |
| * @param newOfflineFilePathMap Map of id to new file path for those offline pages that are |
| * published before sharing. |
| * @return Intent that can be used to share the items. |
| */ |
| public static Intent createShareIntent( |
| List<DownloadHistoryItemWrapper> items, Map<String, String> newOfflineFilePathMap) { |
| Intent shareIntent = new Intent(); |
| String intentAction; |
| ArrayList<Uri> itemUris = new ArrayList<Uri>(); |
| StringBuilder offlinePagesString = new StringBuilder(); |
| @DownloadFilter.Type |
| int selectedItemsFilterType = items.get(0).getFilterType(); |
| |
| String intentMimeType = ""; |
| String[] intentMimeParts = {"", ""}; |
| |
| Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| if (activity != null && activity instanceof ChromeTabbedActivity) { |
| ChromeTabbedActivity chromeActivity = ((ChromeTabbedActivity) activity); |
| ScreenshotTabObserver tabObserver = |
| ScreenshotTabObserver.from(chromeActivity.getActivityTab()); |
| if (tabObserver != null) { |
| tabObserver.onActionPerformedAfterScreenshot( |
| ScreenshotTabObserver.SCREENSHOT_ACTION_SHARE); |
| } |
| } |
| |
| for (int i = 0; i < items.size(); i++) { |
| DownloadHistoryItemWrapper wrappedItem = items.get(i); |
| String mimeType = Intent.normalizeMimeType(wrappedItem.getMimeType()); |
| |
| if (wrappedItem.isOfflinePage()) { |
| // Attempt to share the mhtml file. If that fails, share by URL. |
| OfflineItemWrapper wrappedOfflineItem = (OfflineItemWrapper) wrappedItem; |
| Uri uriToShare = |
| getUriToShareOfflinePage(wrappedOfflineItem, newOfflineFilePathMap); |
| |
| if (uriToShare == null) { |
| // Share the URL, instead of the file, if publishing the file failed. |
| if (offlinePagesString.length() != 0) { |
| offlinePagesString.append("\n"); |
| } |
| offlinePagesString.append(wrappedItem.getUrl()); |
| mimeType = MIME_TYPE_SHARING_URL; |
| } else { |
| itemUris.add(uriToShare); |
| RecordUserAction.record("OfflinePages.Sharing.SharePageFromDownloadHome"); |
| } |
| } else { |
| // If not sharing an offline page, generate the URI for the file being shared. |
| itemUris.add(getUriForItem(wrappedItem.getFilePath())); |
| } |
| |
| if (selectedItemsFilterType != wrappedItem.getFilterType()) { |
| selectedItemsFilterType = DownloadFilter.Type.ALL; |
| } |
| if (wrappedItem.getFilterType() == DownloadFilter.Type.OTHER) { |
| RecordHistogram.recordEnumeratedHistogram( |
| "Android.DownloadManager.OtherExtensions.Share", |
| wrappedItem.getFileExtensionType(), FileExtensions.Type.NUM_ENTRIES); |
| } |
| |
| // If a mime type was not retrieved from the backend or could not be normalized, |
| // set the mime type to the default. |
| if (TextUtils.isEmpty(mimeType)) { |
| intentMimeType = DEFAULT_MIME_TYPE; |
| continue; |
| } |
| |
| // If the intent mime type has not been set yet, set it to the mime type for this item. |
| if (TextUtils.isEmpty(intentMimeType)) { |
| intentMimeType = mimeType; |
| if (!TextUtils.isEmpty(intentMimeType)) { |
| intentMimeParts = intentMimeType.split(MIME_TYPE_DELIMITER); |
| // Guard against invalid mime types. |
| if (intentMimeParts.length != 2) intentMimeType = DEFAULT_MIME_TYPE; |
| } |
| continue; |
| } |
| |
| // Either the mime type is already the default or it matches the current item's mime |
| // type. In either case, intentMimeType is already the correct value. |
| if (TextUtils.equals(intentMimeType, DEFAULT_MIME_TYPE) |
| || TextUtils.equals(intentMimeType, mimeType)) { |
| continue; |
| } |
| |
| String[] mimeParts = mimeType.split(MIME_TYPE_DELIMITER); |
| if (!TextUtils.equals(intentMimeParts[0], mimeParts[0])) { |
| // The top-level types don't match; fallback to the default mime type. |
| intentMimeType = DEFAULT_MIME_TYPE; |
| } else { |
| // The mime type should be {top-level type}/* |
| intentMimeType = intentMimeParts[0] + MIME_TYPE_DELIMITER + "*"; |
| } |
| } |
| |
| // Use Action_SEND if there is only one downloaded item or only text to share. |
| if (itemUris.size() == 0 || (itemUris.size() == 1 && offlinePagesString.length() == 0)) { |
| intentAction = Intent.ACTION_SEND; |
| } else { |
| intentAction = Intent.ACTION_SEND_MULTIPLE; |
| } |
| |
| if (itemUris.size() == 1) { |
| // Sharing a downloaded item or an offline page. |
| shareIntent.putExtra(Intent.EXTRA_STREAM, itemUris.get(0)); |
| } else if (itemUris.size() > 1) { |
| shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, itemUris); |
| } |
| |
| if (offlinePagesString.length() != 0) { |
| shareIntent.putExtra(Intent.EXTRA_TEXT, offlinePagesString.toString()); |
| } |
| |
| // If there is exactly one item shared, set the mail title. |
| if (items.size() == 1) { |
| shareIntent.putExtra(Intent.EXTRA_SUBJECT, items.get(0).getDisplayFileName()); |
| } |
| |
| shareIntent.setAction(intentAction); |
| shareIntent.setType(intentMimeType); |
| shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| |
| recordShareHistograms(items.size(), selectedItemsFilterType); |
| |
| return shareIntent; |
| } |
| |
| /** |
| * Compute the URI to use for sharing this page. |
| * @param wrappedOfflineItem OfflineItem to be shared. |
| * @param newOfflineFilePathMap map of offline id to the file path now that publishing is done. |
| * @return Uri to use for sharing this offline page, or null if we cannot build one. |
| */ |
| private static Uri getUriToShareOfflinePage( |
| OfflineItemWrapper wrappedOfflineItem, Map<String, String> newOfflineFilePathMap) { |
| String newFilePath = wrappedOfflineItem.getFilePath(); |
| |
| if (wrappedOfflineItem.isSuggested()) { |
| // If we have a temporary page, share it by content URI. Today this only |
| // supports suggested pages, since they are the only type of temporary pages |
| // shown in DownloadsHome. If we support other types of pages someday, |
| // we'll need to add support for them here too. |
| try { |
| return (new FileProviderHelper()).getContentUriFromFile(new File(newFilePath)); |
| } catch (Exception e) { |
| return null; |
| } |
| } |
| |
| if (newOfflineFilePathMap == null) { |
| // If the file was already in the public directory, use the existing file path. |
| return getUriForItem(wrappedOfflineItem.getFilePath()); |
| } |
| |
| String publishedFilePath = newOfflineFilePathMap.get(wrappedOfflineItem.getId()); |
| if (!TextUtils.isEmpty(publishedFilePath)) { |
| // If we moved the file to publish it, use the new path. |
| return getUriForItem(publishedFilePath); |
| } |
| |
| // If publishing failed, return null, and we will share by original URL. |
| return null; |
| } |
| |
| /** |
| * Performs all the necessary work needed to share download items. For offline pages, we may |
| * need to publish the internal archive file to public location first. |
| * |
| * @param items Items to share. |
| * @return True if the work is done or not needed and the sharing can start immediately. |
| * False if the asynchronous work is in progress. After it is done, |callback| will be |
| * invoked to inform the result. |
| */ |
| public static boolean prepareForSharing( |
| List<DownloadHistoryItemWrapper> items, Callback<Map<String, String>> callback) { |
| OfflinePageBridge offlinePageBridge = |
| OfflinePageBridge.getForProfile(Profile.getLastUsedProfile().getOriginalProfile()); |
| |
| // If the sharing of offline pages is enabled, we need to publish the archive files if they |
| // are still located in the internal directory, and not temporary pages. |
| List<OfflineItemWrapper> offlinePagesToPublish = new ArrayList<OfflineItemWrapper>(); |
| for (int i = 0; i < items.size(); i++) { |
| DownloadHistoryItemWrapper wrappedItem = items.get(i); |
| if (wrappedItem.isOfflinePage()) { |
| OfflineItemWrapper wrappedOfflineItem = (OfflineItemWrapper) wrappedItem; |
| if (!wrappedOfflineItem.isSuggested() |
| && offlinePageBridge.isInPrivateDirectory( |
| wrappedOfflineItem.getFilePath())) { |
| offlinePagesToPublish.add(wrappedOfflineItem); |
| } |
| } |
| } |
| |
| if (offlinePagesToPublish.isEmpty()) return true; |
| |
| publishOfflinePagesForSharing(offlinePageBridge, offlinePagesToPublish, callback); |
| return false; |
| } |
| |
| static void publishOfflinePagesForSharing(OfflinePageBridge offlinePageBridge, |
| List<OfflineItemWrapper> offlinePages, Callback<Map<String, String>> callback) { |
| DownloadController.requestFileAccessPermission(granted -> { |
| if (!granted) { |
| OfflinePageUtils.recordPublishPageResult(SavePageResult.PERMISSION_DENIED); |
| return; |
| } |
| publishOfflinePageForSharing( |
| offlinePageBridge, offlinePages, 0, new HashMap<String, String>(), callback); |
| }); |
| } |
| |
| static void publishOfflinePageForSharing(OfflinePageBridge offlinePageBridge, |
| final List<OfflineItemWrapper> offlinePages, final int index, |
| final Map<String, String> newFilePathMap, Callback<Map<String, String>> callback) { |
| assert index < offlinePages.size(); |
| |
| final OfflineItemWrapper wrappedItem = offlinePages.get(index); |
| assert wrappedItem.isOfflinePage(); |
| |
| offlinePageBridge.publishInternalPageByGuid(wrappedItem.getId(), (newFilePath) -> { |
| if (!newFilePath.isEmpty()) { |
| newFilePathMap.put(wrappedItem.getId(), newFilePath); |
| } |
| |
| int nextIndex = index + 1; |
| if (nextIndex >= offlinePages.size()) { |
| callback.onResult(newFilePathMap); |
| return; |
| } |
| |
| publishOfflinePageForSharing( |
| offlinePageBridge, offlinePages, nextIndex, newFilePathMap, callback); |
| }); |
| } |
| |
| /** |
| * Returns a URI that points at the file. |
| * @param filePath File path to get a URI for. |
| * @return URI that points at that file, either as a content:// URI or a file:// URI. |
| */ |
| public static Uri getUriForItem(String filePath) { |
| if (ContentUriUtils.isContentUri(filePath)) return Uri.parse(filePath); |
| |
| Uri uri = null; |
| |
| // FileUtils.getUriForFile() causes a disk read when it calls into |
| // FileProvider#getUriForFile. Obtaining a content URI is on the critical path for creating |
| // a share intent after the user taps on the share button, so even if we were to run this |
| // method on a background thread we would have to wait. As it depends on user-selected |
| // items, we cannot know/preload which URIs we need until the user presses share. |
| StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); |
| uri = FileUtils.getUriForFile(new File(filePath)); |
| StrictMode.setThreadPolicy(oldPolicy); |
| |
| return uri; |
| } |
| |
| @CalledByNative |
| private static String getUriStringForPath(String filePath) { |
| if (ContentUriUtils.isContentUri(filePath)) return filePath; |
| Uri uri = getUriForItem(filePath); |
| return uri != null ? uri.toString() : new String(); |
| } |
| |
| /** |
| * If the given MIME type is null, or one of the "generic" types (text/plain |
| * or application/octet-stream) map it to a type that Android can deal with. |
| * If the given type is not generic, return it unchanged. |
| * See {@code ChromeDownloadDelegate#remapGenericMimeType}. |
| * |
| * @param mimeType MIME type provided by the server. |
| * @param url URL of the data being loaded. |
| * @param filename file name obtained from content disposition header |
| * @return The MIME type that should be used for this data. |
| */ |
| @CalledByNative |
| public static String remapGenericMimeType(String mimeType, String url, String filename) { |
| if (TextUtils.isEmpty(mimeType)) mimeType = UNKNOWN_MIME_TYPE; |
| return ChromeDownloadDelegate.remapGenericMimeType(mimeType, url, filename); |
| } |
| |
| /** |
| * Utility method to open an {@link OfflineItem}, which can be a chrome download, offline page. |
| * Falls back to open download home. |
| * @param contentId The {@link ContentId} of the associated offline item. |
| */ |
| public static void openItem(ContentId contentId, boolean isOffTheRecord, |
| @DownloadMetrics.DownloadOpenSource int source) { |
| if (LegacyHelpers.isLegacyOfflinePage(contentId)) { |
| OfflineContentAggregatorFactory.forProfile(Profile.getLastUsedProfile()) |
| .openItem(LaunchLocation.PROGRESS_BAR, contentId); |
| } else { |
| DownloadManagerService.getDownloadManagerService().openDownload( |
| contentId, isOffTheRecord, source); |
| } |
| } |
| |
| /** |
| * Opens a file in Chrome or in another app if appropriate. |
| * @param filePath Path to the file to open, can be a content Uri. |
| * @param mimeType mime type of the file. |
| * @param downloadGuid The associated download GUID. |
| * @param isOffTheRecord whether we are in an off the record context. |
| * @param originalUrl The original url of the downloaded file. |
| * @param referrer Referrer of the downloaded file. |
| * @param source The source that tries to open the download file. |
| * @return whether the file could successfully be opened. |
| */ |
| public static boolean openFile(String filePath, String mimeType, String downloadGuid, |
| boolean isOffTheRecord, String originalUrl, String referrer, |
| @DownloadMetrics.DownloadOpenSource int source) { |
| DownloadMetrics.recordDownloadOpen(source, mimeType); |
| Context context = ContextUtils.getApplicationContext(); |
| DownloadManagerService service = DownloadManagerService.getDownloadManagerService(); |
| Uri contentUri = getUriForItem(filePath); |
| |
| // Check if Chrome should open the file itself. |
| if (service.isDownloadOpenableInBrowser(isOffTheRecord, mimeType)) { |
| // Share URIs use the content:// scheme when able, which looks bad when displayed |
| // in the URL bar. |
| Uri fileUri = contentUri; |
| if (!ContentUriUtils.isContentUri(filePath)) { |
| File file = new File(filePath); |
| fileUri = Uri.fromFile(file); |
| } |
| String normalizedMimeType = Intent.normalizeMimeType(mimeType); |
| |
| Intent intent = MediaViewerUtils.getMediaViewerIntent( |
| fileUri, contentUri, normalizedMimeType, true /* allowExternalAppHandlers */); |
| IntentHandler.startActivityForTrustedIntent(intent); |
| service.updateLastAccessTime(downloadGuid, isOffTheRecord); |
| return true; |
| } |
| |
| // Check if any apps can open the file. |
| try { |
| // TODO(qinmin): Move this to an AsyncTask so we don't need to temper with strict mode. |
| StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); |
| Uri uri = ContentUriUtils.isContentUri(filePath) |
| ? contentUri |
| : ApiCompatibilityUtils.getUriForDownloadedFile(new File(filePath)); |
| StrictMode.setThreadPolicy(oldPolicy); |
| Intent viewIntent = |
| MediaViewerUtils.createViewIntentForUri(uri, mimeType, originalUrl, referrer); |
| context.startActivity(viewIntent); |
| service.updateLastAccessTime(downloadGuid, isOffTheRecord); |
| return true; |
| } catch (Exception e) { |
| // Can't launch the Intent. |
| if (source != DownloadMetrics.DownloadOpenSource.DOWNLOAD_PROGRESS_INFO_BAR) { |
| Toast.makeText(context, context.getString(R.string.download_cant_open_file), |
| Toast.LENGTH_SHORT) |
| .show(); |
| } |
| return false; |
| } |
| } |
| |
| @CalledByNative |
| private static void openDownload(String filePath, String mimeType, String downloadGuid, |
| boolean isOffTheRecord, String originalUrl, String referer, |
| @DownloadMetrics.DownloadOpenSource int source) { |
| boolean canOpen = DownloadUtils.openFile( |
| filePath, mimeType, downloadGuid, isOffTheRecord, originalUrl, referer, source); |
| if (!canOpen) { |
| DownloadUtils.showDownloadManager(null, null); |
| } |
| } |
| |
| private static void recordShareHistograms(int count, int filterType) { |
| RecordHistogram.recordEnumeratedHistogram("Android.DownloadManager.Share.FileTypes", |
| filterType, DownloadFilter.Type.NUM_ENTRIES); |
| |
| RecordHistogram.recordLinearCountHistogram("Android.DownloadManager.Share.Count", |
| count, 1, 20, 20); |
| } |
| |
| /** |
| * Fires an Intent to open a downloaded item. |
| * @param context Context to use. |
| * @param intent Intent that can be fired. |
| * @return Whether an Activity was successfully started for the Intent. |
| */ |
| static boolean fireOpenIntentForDownload(Context context, Intent intent) { |
| try { |
| if (TextUtils.equals(intent.getPackage(), context.getPackageName())) { |
| IntentHandler.startActivityForTrustedIntent(intent); |
| } else { |
| context.startActivity(intent); |
| } |
| return true; |
| } catch (ActivityNotFoundException ex) { |
| Log.d(TAG, "Activity not found for " + intent.getType() + " over " |
| + intent.getData().getScheme(), ex); |
| } catch (SecurityException ex) { |
| Log.d(TAG, "cannot open intent: " + intent, ex); |
| } catch (Exception ex) { |
| Log.d(TAG, "cannot open intent: " + intent, ex); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Helper method to determine the progress text to use for an in progress download notification. |
| * @param progress The {@link Progress} struct that represents the current state of an in |
| * progress download. |
| * @return The {@link String} that represents the progress. |
| */ |
| public static String getProgressTextForNotification(Progress progress) { |
| Context context = ContextUtils.getApplicationContext(); |
| |
| if (progress.isIndeterminate() && progress.value == 0) { |
| return context.getResources().getString(R.string.download_started); |
| } |
| |
| switch (progress.unit) { |
| case OfflineItemProgressUnit.PERCENTAGE: |
| return progress.isIndeterminate() |
| ? context.getResources().getString(R.string.download_started) |
| : getPercentageString(progress.getPercentage()); |
| case OfflineItemProgressUnit.BYTES: |
| String bytes = getStringForBytes(context, progress.value); |
| if (progress.isIndeterminate()) { |
| return context.getResources().getString( |
| R.string.download_ui_indeterminate_bytes, bytes); |
| } else { |
| String total = getStringForBytes(context, progress.max); |
| return context.getResources().getString( |
| R.string.download_ui_determinate_bytes, bytes, total); |
| } |
| case OfflineItemProgressUnit.FILES: |
| if (progress.isIndeterminate()) { |
| int fileCount = (int) Math.min(Integer.MAX_VALUE, progress.value); |
| return context.getResources().getQuantityString( |
| R.plurals.download_ui_files_downloaded, fileCount, fileCount); |
| } else { |
| return formatRemainingFiles(context, progress); |
| } |
| default: |
| assert false; |
| } |
| |
| return ""; |
| } |
| |
| /** |
| * Create a string that represents the percentage of the file that has downloaded. |
| * @param percentage Current percentage of the file. |
| * @return String representing the percentage of the file that has been downloaded. |
| */ |
| public static String getPercentageString(int percentage) { |
| NumberFormat formatter = NumberFormat.getPercentInstance(Locale.getDefault()); |
| return formatter.format(percentage / 100.0); |
| } |
| |
| /** |
| * Creates a string that shows the time left or number of files left. |
| * @param context The application context. |
| * @param progress The download progress. |
| * @param timeRemainingInMillis The remaining time in milli seconds. |
| * @return Formatted string representing the time left or the number of files left. |
| */ |
| public static String getTimeOrFilesLeftString( |
| Context context, Progress progress, long timeRemainingInMillis) { |
| return progress.unit == OfflineItemProgressUnit.FILES |
| ? formatRemainingFiles(context, progress) |
| : formatRemainingTime(context, timeRemainingInMillis); |
| } |
| |
| /** |
| * Creates a string that represents the number of files left to be downloaded. |
| * @param progress Current download progress. |
| * @return String representing the number of files left. |
| */ |
| public static String formatRemainingFiles(Context context, Progress progress) { |
| int filesLeft = (int) (progress.max - progress.value); |
| return filesLeft == 1 ? context.getResources().getString(R.string.one_file_left) |
| : context.getResources().getString(R.string.files_left, filesLeft); |
| } |
| |
| /** |
| * Format remaining time for the given millis, in the following format: |
| * 5 hours; will include 1 unit, can go down to seconds precision. |
| * This is similar to what android.java.text.Formatter.formatShortElapsedTime() does. Don't use |
| * ui::TimeFormat::Simple() as it is very expensive. |
| * |
| * @param context the application context. |
| * @param millis the remaining time in milli seconds. |
| * @return the formatted remaining time. |
| */ |
| public static String formatRemainingTime(Context context, long millis) { |
| long secondsLong = millis / 1000; |
| |
| int days = 0; |
| int hours = 0; |
| int minutes = 0; |
| if (secondsLong >= TimeUtils.SECONDS_PER_DAY) { |
| days = (int) (secondsLong / TimeUtils.SECONDS_PER_DAY); |
| secondsLong -= days * TimeUtils.SECONDS_PER_DAY; |
| } |
| if (secondsLong >= TimeUtils.SECONDS_PER_HOUR) { |
| hours = (int) (secondsLong / TimeUtils.SECONDS_PER_HOUR); |
| secondsLong -= hours * TimeUtils.SECONDS_PER_HOUR; |
| } |
| if (secondsLong >= TimeUtils.SECONDS_PER_MINUTE) { |
| minutes = (int) (secondsLong / TimeUtils.SECONDS_PER_MINUTE); |
| secondsLong -= minutes * TimeUtils.SECONDS_PER_MINUTE; |
| } |
| int seconds = (int) secondsLong; |
| |
| if (days >= 2) { |
| days += (hours + 12) / 24; |
| return context.getString(R.string.remaining_duration_days, days); |
| } else if (days > 0) { |
| return context.getString(R.string.remaining_duration_one_day); |
| } else if (hours >= 2) { |
| hours += (minutes + 30) / 60; |
| return context.getString(R.string.remaining_duration_hours, hours); |
| } else if (hours > 0) { |
| return context.getString(R.string.remaining_duration_one_hour); |
| } else if (minutes >= 2) { |
| minutes += (seconds + 30) / 60; |
| return context.getString(R.string.remaining_duration_minutes, minutes); |
| } else if (minutes > 0) { |
| return context.getString(R.string.remaining_duration_one_minute); |
| } else if (seconds == 1) { |
| return context.getString(R.string.remaining_duration_one_second); |
| } else { |
| return context.getString(R.string.remaining_duration_seconds, seconds); |
| } |
| } |
| |
| /** |
| * Determine what String to show for a given offline page in download home. |
| * @param item The offline item representing the offline page. |
| * @return String representing the current status. |
| */ |
| public static String getOfflinePageStatusString(OfflineItem item) { |
| Context context = ContextUtils.getApplicationContext(); |
| switch (item.state) { |
| case OfflineItemState.COMPLETE: |
| return context.getString(R.string.download_notification_completed); |
| case OfflineItemState.PENDING: |
| return getPendingStatusString(item.pendingState); |
| case OfflineItemState.PAUSED: |
| return context.getString(R.string.download_notification_paused); |
| case OfflineItemState.IN_PROGRESS: // intentional fall through |
| case OfflineItemState.CANCELLED: // intentional fall through |
| case OfflineItemState.INTERRUPTED: // intentional fall through |
| case OfflineItemState.FAILED: |
| break; |
| // case OfflineItemState.MAX_DOWNLOAD_STATE: |
| default: |
| assert false : "Unexpected OfflineItemState: " + item.state; |
| } |
| |
| long bytesReceived = item.receivedBytes; |
| if (bytesReceived == 0) { |
| return context.getString(R.string.download_started); |
| } |
| |
| return DownloadUtils.getStringForDownloadedBytes(context, bytesReceived); |
| } |
| |
| /** |
| * Determine what String to show for a given download in download home. |
| * @param item Download to check the status of. |
| * @return String representing the current download status. |
| */ |
| public static String getStatusString(DownloadItem item) { |
| Context context = ContextUtils.getApplicationContext(); |
| DownloadInfo info = item.getDownloadInfo(); |
| Progress progress = info.getProgress(); |
| |
| int state = info.state(); |
| if (state == DownloadState.COMPLETE) { |
| return context.getString(R.string.download_notification_completed); |
| } |
| |
| if (isDownloadPending(item)) { |
| // All pending, non-offline page downloads are by default waiting for network. |
| // The other pending reason (i.e. waiting for another download to complete) applies |
| // only to offline page requests because offline pages download one at a time. |
| return getPendingStatusString(PendingState.PENDING_NETWORK); |
| } else if (isDownloadPaused(item)) { |
| return context.getString(R.string.download_notification_paused); |
| } |
| |
| if (info.getBytesReceived() == 0 |
| || (!item.isIndeterminate() && info.getTimeRemainingInMillis() < 0)) { |
| // We lack enough information about the download to display a useful string. |
| return context.getString(R.string.download_started); |
| } else if (item.isIndeterminate()) { |
| // Count up the bytes. |
| long bytes = info.getBytesReceived(); |
| return DownloadUtils.getStringForDownloadedBytes(context, bytes); |
| } else { |
| // Count down the time or number of files. |
| return getTimeOrFilesLeftString(context, progress, info.getTimeRemainingInMillis()); |
| } |
| } |
| |
| /** |
| * Determine the status string for a failed download. |
| * |
| * @param failState Reason download failed. |
| * @return String representing the current download status. |
| */ |
| public static String getFailStatusString(@FailState int failState) { |
| if (BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted()) { |
| return nativeGetFailStateMessage(failState); |
| } |
| Context context = ContextUtils.getApplicationContext(); |
| return context.getString(R.string.download_notification_failed); |
| } |
| |
| /** |
| * Determine the status string for a pending download. |
| * |
| * @param pendingState Reason download is pending. |
| * @return String representing the current download status. |
| */ |
| public static String getPendingStatusString(@PendingState int pendingState) { |
| Context context = ContextUtils.getApplicationContext(); |
| // When foreground service restarts and there is no connection to native, use the default |
| // pending status. The status will be replaced when connected to native. |
| if (BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted() |
| && ChromeFeatureList.isEnabled( |
| ChromeFeatureList.OFFLINE_PAGES_DESCRIPTIVE_PENDING_STATUS)) { |
| switch (pendingState) { |
| case PendingState.PENDING_NETWORK: |
| return context.getString(R.string.download_notification_pending_network); |
| case PendingState.PENDING_ANOTHER_DOWNLOAD: |
| return context.getString( |
| R.string.download_notification_pending_another_download); |
| default: |
| return context.getString(R.string.download_notification_pending); |
| } |
| } else { |
| return context.getString(R.string.download_notification_pending); |
| } |
| } |
| |
| /** |
| * Get the resume mode based on the current fail state, to distinguish the case where download |
| * cannot be resumed at all or can be resumed in the middle, or should be restarted from the |
| * beginning. |
| * @param url URL of the download. |
| * @param failState Why the download failed. |
| * @return The resume mode for the current fail state. |
| */ |
| public static @ResumeMode int getResumeMode(String url, @FailState int failState) { |
| return nativeGetResumeMode(url, failState); |
| } |
| |
| /** |
| * Query the Download backends about whether a download is paused. |
| * |
| * The Java-side contains more information about the status of a download than is persisted |
| * by the native backend, so it is queried first. |
| * |
| * @param item Download to check the status of. |
| * @return Whether the download is paused or not. |
| */ |
| public static boolean isDownloadPaused(DownloadItem item) { |
| DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance(); |
| DownloadSharedPreferenceEntry entry = |
| helper.getDownloadSharedPreferenceEntry(item.getContentId()); |
| |
| if (entry != null) { |
| // The Java downloads backend knows more about the download than the native backend. |
| return !entry.isAutoResumable; |
| } else { |
| // Only the native downloads backend knows about the download. |
| if (item.getDownloadInfo().state() == DownloadState.IN_PROGRESS) { |
| return item.getDownloadInfo().isPaused(); |
| } else { |
| return item.getDownloadInfo().state() == DownloadState.INTERRUPTED; |
| } |
| } |
| } |
| |
| /** |
| * Return whether a download is pending. |
| * @param item Download to check the status of. |
| * @return Whether the download is pending or not. |
| */ |
| public static boolean isDownloadPending(DownloadItem item) { |
| DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance(); |
| DownloadSharedPreferenceEntry entry = |
| helper.getDownloadSharedPreferenceEntry(item.getContentId()); |
| return entry != null && item.getDownloadInfo().state() == DownloadState.INTERRUPTED |
| && entry.isAutoResumable; |
| } |
| |
| /** |
| * Format the number of bytes into KB, or MB, or GB and return the corresponding string |
| * resource. Uses default download-related set of strings. |
| * @param context Context to use. |
| * @param bytes Number of bytes. |
| * @return A formatted string to be displayed. |
| */ |
| public static String getStringForDownloadedBytes(Context context, long bytes) { |
| return getStringForBytes(context, BYTES_DOWNLOADED_STRINGS, bytes); |
| } |
| |
| /** |
| * Format the number of available bytes into KB, MB, or GB and return the corresponding string |
| * resource. Uses default format "20 KB available." |
| * |
| * @param context Context to use. |
| * @param bytes Number of bytes needed to display. |
| * @return The formatted string to be displayed. |
| */ |
| public static String getStringForAvailableBytes(Context context, long bytes) { |
| return getStringForBytes(context, BYTES_AVAILABLE_STRINGS, bytes); |
| } |
| |
| /** |
| * Format the number of bytes into KB, MB, or GB and return the corresponding generated string. |
| * @param context Context to use. |
| * @param bytes Number of bytes needed to display. |
| * @return The formatted string to be displayed. |
| */ |
| public static String getStringForBytes(Context context, long bytes) { |
| return getStringForBytes(context, BYTES_STRINGS, bytes); |
| } |
| |
| /** |
| * Format the number of bytes into KB, or MB, or GB and return the corresponding string |
| * resource. |
| * @param context Context to use. |
| * @param stringSet The string resources for displaying bytes in KB, MB and GB. |
| * @param bytes Number of bytes. |
| * @return A formatted string to be displayed. |
| */ |
| public static String getStringForBytes(Context context, int[] stringSet, long bytes) { |
| int resourceId; |
| float bytesInCorrectUnits; |
| |
| if (ConversionUtils.bytesToMegabytes(bytes) < 1) { |
| resourceId = stringSet[0]; |
| bytesInCorrectUnits = bytes / (float) ConversionUtils.BYTES_PER_KILOBYTE; |
| } else if (ConversionUtils.bytesToGigabytes(bytes) < 1) { |
| resourceId = stringSet[1]; |
| bytesInCorrectUnits = bytes / (float) ConversionUtils.BYTES_PER_MEGABYTE; |
| } else { |
| resourceId = stringSet[2]; |
| bytesInCorrectUnits = bytes / (float) ConversionUtils.BYTES_PER_GIGABYTE; |
| } |
| |
| return context.getResources().getString(resourceId, bytesInCorrectUnits); |
| } |
| |
| /** |
| * Abbreviate a file name into a given number of characters with ellipses. |
| * e.g. "thisisaverylongfilename.txt" => "thisisave....txt". |
| * @param fileName File name to abbreviate. |
| * @param limit Character limit. |
| * @return Abbreviated file name. |
| */ |
| public static String getAbbreviatedFileName(String fileName, int limit) { |
| assert limit >= 1; // Abbreviated file name should at least be 1 characters (a...) |
| |
| if (TextUtils.isEmpty(fileName) || fileName.length() <= limit) return fileName; |
| |
| // Find the file name extension |
| int index = fileName.lastIndexOf("."); |
| int extensionLength = fileName.length() - index; |
| |
| // If the extension is too long, just use truncate the string from beginning. |
| if (extensionLength >= limit) { |
| return fileName.substring(0, limit) + ELLIPSIS; |
| } |
| int remainingLength = limit - extensionLength; |
| return fileName.substring(0, remainingLength) + ELLIPSIS + fileName.substring(index); |
| } |
| |
| /** |
| * Return an icon for a given file type. |
| * @param fileType Type of the file as returned by DownloadFilter. |
| * @param iconSize Size of the returned icon. |
| * @return Resource ID of the corresponding icon. |
| */ |
| public static int getIconResId(int fileType, @IconSize int iconSize) { |
| // TODO(huayinz): Make image view size same as icon size so that 36dp icons can be removed. |
| switch (fileType) { |
| case DownloadFilter.Type.PAGE: |
| return iconSize == IconSize.DP_24 ? R.drawable.ic_globe_24dp |
| : R.drawable.ic_globe_36dp; |
| case DownloadFilter.Type.VIDEO: |
| return iconSize == IconSize.DP_24 ? R.drawable.ic_videocam_24dp |
| : R.drawable.ic_videocam_36dp; |
| case DownloadFilter.Type.AUDIO: |
| return iconSize == IconSize.DP_24 ? R.drawable.ic_music_note_24dp |
| : R.drawable.ic_music_note_36dp; |
| case DownloadFilter.Type.IMAGE: |
| return iconSize == IconSize.DP_24 ? R.drawable.ic_drive_image_24dp |
| : R.drawable.ic_drive_image_36dp; |
| case DownloadFilter.Type.DOCUMENT: |
| return iconSize == IconSize.DP_24 ? R.drawable.ic_drive_document_24dp |
| : R.drawable.ic_drive_document_36dp; |
| default: |
| return iconSize == IconSize.DP_24 ? R.drawable.ic_drive_file_24dp |
| : R.drawable.ic_drive_file_36dp; |
| } |
| } |
| |
| /** |
| * Return a background color for the file type icon. |
| * @param context Context from which to extract the resources. |
| * @return Background color. |
| */ |
| public static int getIconBackgroundColor(Context context) { |
| return ApiCompatibilityUtils.getColor(context.getResources(), R.color.light_active_color); |
| } |
| |
| /** |
| * Return a foreground color list for the file type icon. |
| * @param context Context from which to extract the resources. |
| * @return a foreground color list. |
| */ |
| public static ColorStateList getIconForegroundColorList(Context context) { |
| return AppCompatResources.getColorStateList(context, R.color.white_mode_tint); |
| } |
| |
| /** |
| * Return if a download item is already viewed by the user. Will return false if the last |
| * access time is not available. |
| * @param item The download item. |
| * @return true if the item is viewed by the user. |
| */ |
| public static boolean isDownloadViewed(DownloadItem item) { |
| if (item == null || item.getDownloadInfo() == null) return false; |
| return item.getDownloadInfo().getLastAccessTime() != 0; |
| } |
| |
| /** |
| * Returns |true| if the offline item is not null and has already been viewed by the user. |
| * @param offlineItem The offline item to check. |
| * @return true if the item is valid has been viewed by the user. |
| */ |
| public static boolean isOfflineItemViewed(OfflineItem offlineItem) { |
| return offlineItem != null && offlineItem.lastAccessedTimeMs > offlineItem.completionTimeMs; |
| } |
| |
| /** |
| * Given two timestamps, calculates if both occur on the same date. |
| * @return True if they belong in the same day. False otherwise. |
| */ |
| public static boolean isSameDay(long timestamp1, long timestamp2) { |
| return getDateAtMidnight(timestamp1).equals(getDateAtMidnight(timestamp2)); |
| } |
| |
| /** |
| * Calculates the {@link Date} for midnight of the date represented by the |timestamp|. |
| */ |
| public static Date getDateAtMidnight(long timestamp) { |
| Calendar cal = Calendar.getInstance(); |
| cal.setTimeInMillis(timestamp); |
| cal.set(Calendar.HOUR_OF_DAY, 0); |
| cal.set(Calendar.MINUTE, 0); |
| cal.set(Calendar.SECOND, 0); |
| cal.set(Calendar.MILLISECOND, 0); |
| return cal.getTime(); |
| } |
| |
| /** |
| * Returns if the path is in the download directory on primary storage. |
| * @param path The directory to check. |
| * @return If the path is in the download directory on primary storage. |
| */ |
| public static boolean isInPrimaryStorageDownloadDirectory(String path) { |
| // Only primary storage can have content URI as file path. |
| if (ContentUriUtils.isContentUri(path)) return true; |
| |
| // Check if the file path contains the external public directory. |
| File primaryDir = null; |
| try (StrictModeContext unused = StrictModeContext.allowDiskReads()) { |
| primaryDir = Environment.getExternalStorageDirectory(); |
| } |
| if (primaryDir == null || path == null) return false; |
| String primaryPath = primaryDir.getAbsolutePath(); |
| return primaryPath == null ? false : path.contains(primaryPath); |
| } |
| |
| /** |
| * Get the primary download directory in public external storage. The directory will be created |
| * if it doesn't exist. |
| * @return The download directory. Can be an invalid directory if failed to create the |
| * directory. |
| */ |
| public static File getPrimaryDownloadDirectory() { |
| File downloadDir = |
| Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); |
| |
| // Create the directory if needed. |
| if (!downloadDir.exists()) { |
| try { |
| downloadDir.mkdirs(); |
| } catch (SecurityException e) { |
| Log.e(TAG, "Exception when creating download directory.", e); |
| } |
| } |
| return downloadDir; |
| } |
| |
| /** |
| * Parses an originating URL string and returns a valid Uri that can be inserted into |
| * DownloadProvider. The returned Uri has to be null or non-empty http(s) scheme. |
| * @param originalUrl String representation of the originating URL. |
| * @return A valid Uri that can be accepted by DownloadProvider. |
| */ |
| public static Uri parseOriginalUrl(String originalUrl) { |
| Uri originalUri = TextUtils.isEmpty(originalUrl) ? null : Uri.parse(originalUrl); |
| if (originalUri != null) { |
| String scheme = originalUri.normalizeScheme().getScheme(); |
| if (scheme == null |
| || (!scheme.equals(UrlConstants.HTTPS_SCHEME) |
| && !scheme.equals(UrlConstants.HTTP_SCHEME))) { |
| originalUri = null; |
| } |
| } |
| return originalUri; |
| } |
| |
| private static native String nativeGetFailStateMessage(@FailState int failState); |
| private static native int nativeGetResumeMode(String url, @FailState int failState); |
| } |