| // Copyright 2015 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.DownloadManager; |
| import android.content.ActivityNotFoundException; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.net.ConnectivityManager; |
| import android.net.NetworkInfo; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Handler; |
| import android.support.annotation.Nullable; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.ObserverList; |
| import org.chromium.base.ThreadUtils; |
| 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.chrome.R; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.download.ui.BackendProvider; |
| import org.chromium.chrome.browser.download.ui.DownloadHistoryAdapter; |
| import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl; |
| import org.chromium.chrome.browser.feature_engagement.TrackerFactory; |
| import org.chromium.chrome.browser.profiles.Profile; |
| 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.LegacyHelpers; |
| import org.chromium.content.browser.BrowserStartupController; |
| import org.chromium.net.ConnectionType; |
| import org.chromium.net.NetworkChangeNotifierAutoDetect; |
| import org.chromium.net.RegistrationPolicyAlwaysRegister; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.RejectedExecutionException; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Chrome implementation of the {@link DownloadController.DownloadNotificationService} interface. |
| * This class is responsible for keeping track of which downloads are in progress. It generates |
| * updates for progress of downloads and handles cleaning up of interrupted progress notifications. |
| * TODO(qinmin): move BroadcastReceiver inheritance into DownloadManagerDelegate, as it handles all |
| * Android DownloadManager interactions. And DownloadManagerService should not know download Id |
| * issued by Android DownloadManager. |
| */ |
| public class DownloadManagerService |
| implements DownloadController.DownloadNotificationService, |
| NetworkChangeNotifierAutoDetect.Observer, |
| DownloadManagerDelegate.DownloadQueryCallback, |
| DownloadManagerDelegate.EnqueueDownloadRequestCallback, DownloadServiceDelegate, |
| BackendProvider.DownloadDelegate { |
| // Download status. |
| public static final int DOWNLOAD_STATUS_IN_PROGRESS = 0; |
| public static final int DOWNLOAD_STATUS_COMPLETE = 1; |
| public static final int DOWNLOAD_STATUS_FAILED = 2; |
| public static final int DOWNLOAD_STATUS_CANCELLED = 3; |
| public static final int DOWNLOAD_STATUS_INTERRUPTED = 4; |
| |
| private static final String TAG = "DownloadService"; |
| private static final String DOWNLOAD_DIRECTORY = "Download"; |
| private static final String UNKNOWN_MIME_TYPE = "application/unknown"; |
| private static final String DOWNLOAD_UMA_ENTRY = "DownloadUmaEntry"; |
| private static final String DOWNLOAD_RETRY_COUNT_FILE_NAME = "DownloadRetryCount"; |
| private static final String DOWNLOAD_MANUAL_RETRY_SUFFIX = ".Manual"; |
| private static final String DOWNLOAD_TOTAL_RETRY_SUFFIX = ".Total"; |
| private static final long UPDATE_DELAY_MILLIS = 1000; |
| // Wait 10 seconds to resume all downloads, so that we won't impact tab loading. |
| private static final long RESUME_DELAY_MILLIS = 10000; |
| private static final int UNKNOWN_DOWNLOAD_STATUS = -1; |
| public static final long UNKNOWN_BYTES_RECEIVED = -1; |
| private static final String PREF_IS_DOWNLOAD_HOME_ENABLED = |
| "org.chromium.chrome.browser.download.IS_DOWNLOAD_HOME_ENABLED"; |
| |
| // Values for the histogram MobileDownloadResumptionCount. |
| private static final int UMA_DOWNLOAD_RESUMPTION_MANUAL_PAUSE = 0; |
| private static final int UMA_DOWNLOAD_RESUMPTION_BROWSER_KILLED = 1; |
| private static final int UMA_DOWNLOAD_RESUMPTION_CLICKED = 2; |
| private static final int UMA_DOWNLOAD_RESUMPTION_FAILED = 3; |
| private static final int UMA_DOWNLOAD_RESUMPTION_AUTO_STARTED = 4; |
| private static final int UMA_DOWNLOAD_RESUMPTION_COUNT = 5; |
| |
| private static final int GB_IN_KILO_BYTES = 1024 * 1024; |
| |
| // Set will be more expensive to initialize, so use an ArrayList here. |
| private static final List<String> MIME_TYPES_TO_OPEN = new ArrayList<String>(Arrays.asList( |
| OMADownloadHandler.OMA_DOWNLOAD_DESCRIPTOR_MIME, |
| "application/pdf", |
| "application/x-x509-ca-cert", |
| "application/x-x509-user-cert", |
| "application/x-x509-server-cert", |
| "application/x-pkcs12", |
| "application/application/x-pem-file", |
| "application/pkix-cert", |
| "application/x-wifi-config")); |
| |
| private static DownloadManagerService sDownloadManagerService; |
| private static boolean sIsNetworkListenerDisabled; |
| private static boolean sIsNetworkMetered; |
| |
| private final SharedPreferences mSharedPrefs; |
| private final HashMap<String, DownloadProgress> mDownloadProgressMap = |
| new HashMap<String, DownloadProgress>(4, 0.75f); |
| |
| private final DownloadNotifier mDownloadNotifier; |
| // Delay between UI updates. |
| private final long mUpdateDelayInMillis; |
| |
| private final Handler mHandler; |
| private final Context mContext; |
| |
| @VisibleForTesting protected final List<String> mAutoResumableDownloadIds = |
| new ArrayList<String>(); |
| private final List<DownloadUmaStatsEntry> mUmaEntries = new ArrayList<DownloadUmaStatsEntry>(); |
| private final ObserverList<DownloadHistoryAdapter> mHistoryAdapters = new ObserverList<>(); |
| |
| private OMADownloadHandler mOMADownloadHandler; |
| private DownloadSnackbarController mDownloadSnackbarController; |
| private long mNativeDownloadManagerService; |
| private DownloadManagerDelegate mDownloadManagerDelegate; |
| private NetworkChangeNotifierAutoDetect mNetworkChangeNotifier; |
| // Flag to track if we need to post a task to update download notifications. |
| private boolean mIsUIUpdateScheduled; |
| private int mAutoResumptionLimit = -1; |
| private DownloadManagerRequestInterceptor mDownloadManagerRequestInterceptor; |
| |
| /** |
| * Interface to intercept download request to Android DownloadManager. This is implemented by |
| * tests so that we don't need to actually enqueue a download into the Android DownloadManager. |
| */ |
| static interface DownloadManagerRequestInterceptor { |
| void interceptDownloadRequest(DownloadItem item, boolean notifyComplete); |
| } |
| |
| /** |
| * Class representing progress of a download. |
| */ |
| private static class DownloadProgress { |
| final long mStartTimeInMillis; |
| boolean mCanDownloadWhileMetered; |
| DownloadItem mDownloadItem; |
| int mDownloadStatus; |
| boolean mIsAutoResumable; |
| boolean mIsUpdated; |
| boolean mIsSupportedMimeType; |
| |
| DownloadProgress(long startTimeInMillis, boolean canDownloadWhileMetered, |
| DownloadItem downloadItem, int downloadStatus) { |
| mStartTimeInMillis = startTimeInMillis; |
| mCanDownloadWhileMetered = canDownloadWhileMetered; |
| mDownloadItem = downloadItem; |
| mDownloadStatus = downloadStatus; |
| mIsAutoResumable = false; |
| mIsUpdated = true; |
| } |
| |
| DownloadProgress(DownloadProgress progress) { |
| mStartTimeInMillis = progress.mStartTimeInMillis; |
| mCanDownloadWhileMetered = progress.mCanDownloadWhileMetered; |
| mDownloadItem = progress.mDownloadItem; |
| mDownloadStatus = progress.mDownloadStatus; |
| mIsAutoResumable = progress.mIsAutoResumable; |
| mIsUpdated = progress.mIsUpdated; |
| mIsSupportedMimeType = progress.mIsSupportedMimeType; |
| } |
| } |
| |
| /** |
| * Creates DownloadManagerService. |
| */ |
| public static DownloadManagerService getDownloadManagerService() { |
| ThreadUtils.assertOnUiThread(); |
| Context appContext = ContextUtils.getApplicationContext(); |
| if (sDownloadManagerService == null) { |
| // TODO(crbug.com/765327): Remove temporary fix after flag is no longer being used. |
| DownloadNotifier downloadNotifier = |
| (!BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted() |
| || !ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOADS_FOREGROUND)) |
| ? new SystemDownloadNotifier(appContext) |
| : new SystemDownloadNotifier2(appContext); |
| sDownloadManagerService = new DownloadManagerService( |
| appContext, downloadNotifier, new Handler(), UPDATE_DELAY_MILLIS); |
| } |
| return sDownloadManagerService; |
| } |
| |
| public static boolean hasDownloadManagerService() { |
| ThreadUtils.assertOnUiThread(); |
| return sDownloadManagerService != null; |
| } |
| |
| /** |
| * For tests only: sets the DownloadManagerService. |
| * @param service An instance of DownloadManagerService. |
| * @return Null or a currently set instance of DownloadManagerService. |
| */ |
| @VisibleForTesting |
| public static DownloadManagerService setDownloadManagerService(DownloadManagerService service) { |
| ThreadUtils.assertOnUiThread(); |
| DownloadManagerService prev = sDownloadManagerService; |
| sDownloadManagerService = service; |
| return prev; |
| } |
| |
| @VisibleForTesting |
| void setDownloadManagerRequestInterceptor(DownloadManagerRequestInterceptor interceptor) { |
| mDownloadManagerRequestInterceptor = interceptor; |
| } |
| |
| @VisibleForTesting |
| protected DownloadManagerService(Context context, DownloadNotifier downloadNotifier, |
| Handler handler, long updateDelayInMillis) { |
| mContext = context; |
| mSharedPrefs = ContextUtils.getAppSharedPreferences(); |
| // Clean up unused shared prefs. TODO(qinmin): remove this after M61. |
| mSharedPrefs.edit().remove(PREF_IS_DOWNLOAD_HOME_ENABLED).apply(); |
| mDownloadNotifier = downloadNotifier; |
| mUpdateDelayInMillis = updateDelayInMillis; |
| mHandler = handler; |
| mDownloadSnackbarController = new DownloadSnackbarController(context); |
| mDownloadManagerDelegate = new DownloadManagerDelegate(mContext); |
| mOMADownloadHandler = new OMADownloadHandler( |
| context, mDownloadManagerDelegate, mDownloadSnackbarController); |
| // Note that this technically leaks the native object, however, DownloadManagerService |
| // is a singleton that lives forever and there's no clean shutdown of Chrome on Android. |
| init(); |
| mOMADownloadHandler.clearPendingOMADownloads(); |
| } |
| |
| @VisibleForTesting |
| protected void init() { |
| DownloadController.setDownloadNotificationService(this); |
| // Post a delayed task to resume all pending downloads. |
| mHandler.postDelayed(() -> mDownloadNotifier.resumePendingDownloads(), RESUME_DELAY_MILLIS); |
| parseUMAStatsEntriesFromSharedPrefs(); |
| Iterator<DownloadUmaStatsEntry> iterator = mUmaEntries.iterator(); |
| boolean hasChanges = false; |
| while (iterator.hasNext()) { |
| DownloadUmaStatsEntry entry = iterator.next(); |
| if (entry.useDownloadManager) { |
| mDownloadManagerDelegate.queryDownloadResult( |
| entry.buildDownloadItem(), false, this); |
| } else if (!entry.isPaused) { |
| entry.isPaused = true; |
| entry.numInterruptions++; |
| hasChanges = true; |
| } |
| } |
| if (hasChanges) { |
| storeUmaEntries(); |
| } |
| } |
| |
| /** |
| * Pre-load shared prefs to avoid being blocked on the disk access async task in the future. |
| */ |
| public static void warmUpSharedPrefs(Context context) { |
| getAutoRetryCountSharedPreference(context); |
| } |
| |
| public DownloadNotifier getDownloadNotifier() { |
| return mDownloadNotifier; |
| } |
| |
| @Override |
| public void onDownloadCompleted(final DownloadInfo downloadInfo) { |
| int status = DOWNLOAD_STATUS_COMPLETE; |
| String mimeType = downloadInfo.getMimeType(); |
| if (downloadInfo.getBytesReceived() == 0) { |
| status = DOWNLOAD_STATUS_FAILED; |
| } else { |
| String origMimeType = mimeType; |
| if (TextUtils.isEmpty(origMimeType)) origMimeType = UNKNOWN_MIME_TYPE; |
| mimeType = ChromeDownloadDelegate.remapGenericMimeType( |
| origMimeType, downloadInfo.getOriginalUrl(), downloadInfo.getFileName()); |
| } |
| DownloadInfo newInfo = |
| DownloadInfo.Builder.fromDownloadInfo(downloadInfo).setMimeType(mimeType).build(); |
| DownloadItem downloadItem = new DownloadItem(false, newInfo); |
| updateDownloadProgress(downloadItem, status); |
| } |
| |
| @Override |
| public void onDownloadUpdated(final DownloadInfo downloadInfo) { |
| DownloadItem item = new DownloadItem(false, downloadInfo); |
| // If user manually paused a download, this download is no longer auto resumable. |
| if (downloadInfo.isPaused()) { |
| removeAutoResumableDownload(item.getId()); |
| } |
| updateDownloadProgress(item, DOWNLOAD_STATUS_IN_PROGRESS); |
| scheduleUpdateIfNeeded(); |
| } |
| |
| @Override |
| public void onDownloadCancelled(final DownloadInfo downloadInfo) { |
| DownloadItem item = new DownloadItem(false, downloadInfo); |
| removeAutoResumableDownload(item.getId()); |
| updateDownloadProgress(new DownloadItem(false, downloadInfo), DOWNLOAD_STATUS_CANCELLED); |
| } |
| |
| @Override |
| public void onDownloadInterrupted(final DownloadInfo downloadInfo, boolean isAutoResumable) { |
| int status = DOWNLOAD_STATUS_INTERRUPTED; |
| DownloadItem item = new DownloadItem(false, downloadInfo); |
| if (!downloadInfo.isResumable()) { |
| status = DOWNLOAD_STATUS_FAILED; |
| } else if (isAutoResumable) { |
| addAutoResumableDownload(item.getId()); |
| } |
| updateDownloadProgress(item, status); |
| |
| DownloadProgress progress = mDownloadProgressMap.get(item.getId()); |
| if (progress == null) return; |
| if (!isAutoResumable || sIsNetworkListenerDisabled) return; |
| ConnectivityManager cm = |
| (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); |
| NetworkInfo info = cm.getActiveNetworkInfo(); |
| if (info == null || !info.isConnected()) return; |
| if (progress.mCanDownloadWhileMetered || !isActiveNetworkMetered(mContext)) { |
| // Normally the download will automatically resume when network is reconnected. |
| // However, if there are multiple network connections and the interruption is caused |
| // by switching between active networks, onConnectionTypeChanged() will not get called. |
| // As a result, we should resume immediately. |
| scheduleDownloadResumption(item); |
| } |
| } |
| |
| /** |
| * Helper method to schedule a download for resumption. |
| * @param item DownloadItem to resume. |
| */ |
| private void scheduleDownloadResumption(final DownloadItem item) { |
| removeAutoResumableDownload(item.getId()); |
| // Post a delayed task to avoid an issue that when connectivity status just changed |
| // to CONNECTED, immediately establishing a connection will sometimes fail. |
| mHandler.postDelayed( |
| () -> resumeDownload(LegacyHelpers.buildLegacyContentId(false, item.getId()), |
| item, false), mUpdateDelayInMillis); |
| } |
| |
| /** |
| * Called when browser activity is launched. For background resumption and cancellation, this |
| * will not be called. |
| */ |
| public void onActivityLaunched() { |
| // TODO(jming): Remove this after M-62. |
| DownloadNotificationService.clearResumptionAttemptLeft(); |
| |
| DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads( |
| /*isOffRecord=*/false); |
| } |
| |
| /** |
| * Broadcast that a download was successful. |
| * @param downloadInfo info about the download. |
| */ |
| protected void broadcastDownloadSuccessful(DownloadInfo downloadInfo) {} |
| |
| /** |
| * Gets download information from SharedPreferences. |
| * @param sharedPrefs The SharedPreferences object to parse. |
| * @param type Type of the information to retrieve. |
| * @return download information saved to the SharedPrefs for the given type. |
| */ |
| @VisibleForTesting |
| protected static Set<String> getStoredDownloadInfo(SharedPreferences sharedPrefs, String type) { |
| return new HashSet<String>(sharedPrefs.getStringSet(type, new HashSet<String>())); |
| } |
| |
| /** |
| * Stores download information to shared preferences. The information can be |
| * either pending download IDs, or pending OMA downloads. |
| * |
| * @param sharedPrefs SharedPreferences to update. |
| * @param type Type of the information. |
| * @param downloadInfo Information to be saved. |
| */ |
| static void storeDownloadInfo( |
| SharedPreferences sharedPrefs, String type, Set<String> downloadInfo) { |
| SharedPreferences.Editor editor = sharedPrefs.edit(); |
| if (downloadInfo.isEmpty()) { |
| editor.remove(type); |
| } else { |
| editor.putStringSet(type, downloadInfo); |
| } |
| editor.apply(); |
| } |
| |
| /** |
| * Updates notifications for a given list of downloads. |
| * @param progresses A list of notifications to update. |
| */ |
| private void updateAllNotifications(List<DownloadProgress> progresses) { |
| assert ThreadUtils.runningOnUiThread(); |
| for (int i = 0; i < progresses.size(); ++i) { |
| updateNotification(progresses.get(i)); |
| } |
| } |
| |
| /** |
| * Update notification for a specific download. |
| * @param progress Specific notification to update. |
| */ |
| private void updateNotification(DownloadProgress progress) { |
| DownloadItem item = progress.mDownloadItem; |
| DownloadInfo info = item.getDownloadInfo(); |
| boolean notificationUpdateScheduled = true; |
| boolean removeFromDownloadProgressMap = true; |
| switch (progress.mDownloadStatus) { |
| case DOWNLOAD_STATUS_COMPLETE: |
| notificationUpdateScheduled = updateDownloadSuccessNotification(progress); |
| removeFromDownloadProgressMap = notificationUpdateScheduled; |
| break; |
| case DOWNLOAD_STATUS_FAILED: |
| mDownloadNotifier.notifyDownloadFailed(info); |
| Log.w(TAG, "Download failed: " + info.getFilePath()); |
| onDownloadFailed(info.getFileName(), DownloadManager.ERROR_UNKNOWN); |
| break; |
| case DOWNLOAD_STATUS_IN_PROGRESS: |
| if (info.isPaused()) { |
| mDownloadNotifier.notifyDownloadPaused(info); |
| recordDownloadResumption(UMA_DOWNLOAD_RESUMPTION_MANUAL_PAUSE); |
| } else { |
| mDownloadNotifier.notifyDownloadProgress( |
| info, progress.mStartTimeInMillis, progress.mCanDownloadWhileMetered); |
| removeFromDownloadProgressMap = false; |
| } |
| break; |
| case DOWNLOAD_STATUS_CANCELLED: |
| mDownloadNotifier.notifyDownloadCanceled(item.getContentId()); |
| break; |
| case DOWNLOAD_STATUS_INTERRUPTED: |
| mDownloadNotifier.notifyDownloadInterrupted(info, progress.mIsAutoResumable); |
| removeFromDownloadProgressMap = !progress.mIsAutoResumable; |
| break; |
| default: |
| assert false; |
| break; |
| } |
| if (notificationUpdateScheduled) { |
| progress.mIsUpdated = false; |
| } |
| if (removeFromDownloadProgressMap) { |
| mDownloadProgressMap.remove(item.getId()); |
| } |
| } |
| |
| /** |
| * Helper method to schedule a task to update the download success notification. |
| * @param progress Download progress to update. |
| * @return True if the task can be scheduled, or false otherwise. |
| */ |
| private boolean updateDownloadSuccessNotification(DownloadProgress progress) { |
| final boolean isSupportedMimeType = progress.mIsSupportedMimeType; |
| final DownloadItem item = progress.mDownloadItem; |
| AsyncTask<Void, Void, Pair<Long, Boolean>> task = |
| new AsyncTask<Void, Void, Pair<Long, Boolean>>() { |
| @Override |
| public Pair<Long, Boolean> doInBackground(Void... params) { |
| boolean success = addCompletedDownload(item); |
| boolean canResolve = success ? (isOMADownloadDescription(item.getDownloadInfo()) |
| || canResolveDownloadItem(mContext, item, isSupportedMimeType)) : false; |
| return Pair.create(item.getSystemDownloadId(), canResolve); |
| } |
| |
| @Override |
| protected void onPostExecute(Pair<Long, Boolean> result) { |
| DownloadInfo info = item.getDownloadInfo(); |
| if (result.first != DownloadItem.INVALID_DOWNLOAD_ID) { |
| mDownloadNotifier.notifyDownloadSuccessful( |
| info, result.first, result.second, isSupportedMimeType); |
| broadcastDownloadSuccessful(info); |
| } else { |
| mDownloadNotifier.notifyDownloadFailed(info); |
| // TODO(qinmin): get the failure message from native. |
| onDownloadFailed(info.getFileName(), DownloadManager.ERROR_UNKNOWN); |
| } |
| } |
| }; |
| try { |
| task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| return true; |
| } catch (RejectedExecutionException e) { |
| // Reaching thread limit, update will be reschduled for the next run. |
| Log.e(TAG, "Thread limit reached, reschedule notification update later."); |
| return false; |
| } |
| } |
| |
| /** |
| * Adds a completed download into Android DownloadManager. |
| * |
| * @param downloadItem Information of the downloaded. |
| * @return true if the download is added to the DownloadManager, or false otherwise. |
| */ |
| protected boolean addCompletedDownload(DownloadItem downloadItem) { |
| assert !ThreadUtils.runningOnUiThread(); |
| DownloadInfo downloadInfo = downloadItem.getDownloadInfo(); |
| String description = downloadInfo.getDescription(); |
| if (TextUtils.isEmpty(description)) description = downloadInfo.getFileName(); |
| try { |
| // Exceptions can be thrown when calling this, although it is not |
| // documented on Android SDK page. |
| long downloadId = mDownloadManagerDelegate.addCompletedDownload( |
| downloadInfo.getFileName(), description, downloadInfo.getMimeType(), |
| downloadInfo.getFilePath(), downloadInfo.getBytesReceived(), |
| downloadInfo.getOriginalUrl(), downloadInfo.getReferrer(), |
| downloadInfo.getDownloadGuid()); |
| downloadItem.setSystemDownloadId(downloadId); |
| return true; |
| } catch (RuntimeException e) { |
| Log.w(TAG, "Failed to add the download item to DownloadManager: ", e); |
| if (downloadInfo.getFilePath() != null) { |
| File file = new File(downloadInfo.getFilePath()); |
| if (!file.delete()) { |
| Log.w(TAG, "Failed to remove the unsuccessful download"); |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Handle auto opennable files after download completes. |
| * TODO(qinmin): move this to DownloadManagerDelegate. |
| * |
| * @param download A download item. |
| */ |
| private void handleAutoOpenAfterDownload(DownloadItem download) { |
| if (isOMADownloadDescription(download.getDownloadInfo())) { |
| mOMADownloadHandler.handleOMADownload( |
| download.getDownloadInfo(), download.getSystemDownloadId()); |
| return; |
| } |
| openDownloadedContent(download.getDownloadInfo(), download.getSystemDownloadId()); |
| } |
| |
| /** |
| * Schedule an update if there is no update scheduled. |
| */ |
| @VisibleForTesting |
| protected void scheduleUpdateIfNeeded() { |
| if (mIsUIUpdateScheduled) return; |
| |
| mIsUIUpdateScheduled = true; |
| final List<DownloadProgress> progressPendingUpdate = new ArrayList<DownloadProgress>(); |
| Iterator<DownloadProgress> iter = mDownloadProgressMap.values().iterator(); |
| while (iter.hasNext()) { |
| DownloadProgress progress = iter.next(); |
| if (progress.mIsUpdated) { |
| progressPendingUpdate.add(progress); |
| } |
| } |
| if (progressPendingUpdate.isEmpty()) { |
| mIsUIUpdateScheduled = false; |
| return; |
| } |
| updateAllNotifications(progressPendingUpdate); |
| |
| Runnable scheduleNextUpdateTask = () -> { |
| mIsUIUpdateScheduled = false; |
| scheduleUpdateIfNeeded(); |
| }; |
| mHandler.postDelayed(scheduleNextUpdateTask, mUpdateDelayInMillis); |
| } |
| |
| /** |
| * Updates the progress of a download. |
| * |
| * @param downloadItem Information about the download. |
| * @param downloadStatus Status of the download. |
| */ |
| private void updateDownloadProgress(DownloadItem downloadItem, int downloadStatus) { |
| boolean isSupportedMimeType = downloadStatus == DOWNLOAD_STATUS_COMPLETE |
| && isSupportedMimeType(downloadItem.getDownloadInfo().getMimeType()); |
| String id = downloadItem.getId(); |
| DownloadProgress progress = mDownloadProgressMap.get(id); |
| long bytesReceived = downloadItem.getDownloadInfo().getBytesReceived(); |
| if (progress == null) { |
| if (!downloadItem.getDownloadInfo().isPaused()) { |
| long startTime = System.currentTimeMillis(); |
| progress = new DownloadProgress( |
| startTime, isActiveNetworkMetered(mContext), downloadItem, downloadStatus); |
| progress.mIsUpdated = true; |
| progress.mIsSupportedMimeType = isSupportedMimeType; |
| mDownloadProgressMap.put(id, progress); |
| DownloadUmaStatsEntry entry = getUmaStatsEntry(downloadItem.getId()); |
| if (entry == null) { |
| addUmaStatsEntry(new DownloadUmaStatsEntry( |
| downloadItem.getId(), startTime, |
| downloadStatus == DOWNLOAD_STATUS_INTERRUPTED ? 1 : 0, false, false, |
| bytesReceived, 0)); |
| } else if (updateBytesReceived(entry, bytesReceived)) { |
| storeUmaEntries(); |
| } |
| |
| // This is mostly for testing, when the download is not tracked/progress is null but |
| // downloadStatus is not DOWNLOAD_STATUS_IN_PROGRESS. |
| if (downloadStatus != DOWNLOAD_STATUS_IN_PROGRESS) { |
| updateNotification(progress); |
| } |
| } |
| return; |
| } |
| |
| progress.mDownloadStatus = downloadStatus; |
| progress.mDownloadItem = downloadItem; |
| progress.mIsUpdated = true; |
| progress.mIsAutoResumable = mAutoResumableDownloadIds.contains(id); |
| progress.mIsSupportedMimeType = isSupportedMimeType; |
| DownloadUmaStatsEntry entry; |
| switch (downloadStatus) { |
| case DOWNLOAD_STATUS_COMPLETE: |
| case DOWNLOAD_STATUS_FAILED: |
| case DOWNLOAD_STATUS_CANCELLED: |
| recordDownloadFinishedUMA(downloadStatus, downloadItem.getId(), |
| downloadItem.getDownloadInfo().getBytesReceived()); |
| clearDownloadRetryCount(downloadItem.getId(), true); |
| clearDownloadRetryCount(downloadItem.getId(), false); |
| updateNotification(progress); |
| break; |
| case DOWNLOAD_STATUS_INTERRUPTED: |
| entry = getUmaStatsEntry(downloadItem.getId()); |
| entry.numInterruptions++; |
| updateBytesReceived(entry, bytesReceived); |
| storeUmaEntries(); |
| updateNotification(progress); |
| break; |
| case DOWNLOAD_STATUS_IN_PROGRESS: |
| entry = getUmaStatsEntry(downloadItem.getId()); |
| if (entry.isPaused != downloadItem.getDownloadInfo().isPaused() |
| || updateBytesReceived(entry, bytesReceived)) { |
| entry.isPaused = downloadItem.getDownloadInfo().isPaused(); |
| storeUmaEntries(); |
| } |
| |
| if (downloadItem.getDownloadInfo().isPaused()) { |
| updateNotification(progress); |
| } |
| break; |
| default: |
| assert false; |
| } |
| } |
| |
| /** |
| * Helper method to update the received bytes and wasted bytes for UMA reporting. |
| * @param entry UMA entry to update. |
| * @param bytesReceived The current received bytes. |
| * @return true if the UMA stats is updated, or false otherwise. |
| */ |
| private boolean updateBytesReceived(DownloadUmaStatsEntry entry, long bytesReceived) { |
| if (bytesReceived == UNKNOWN_BYTES_RECEIVED || bytesReceived == entry.lastBytesReceived) { |
| return false; |
| } |
| if (bytesReceived < entry.lastBytesReceived) { |
| entry.bytesWasted += entry.lastBytesReceived - bytesReceived; |
| } |
| entry.lastBytesReceived = bytesReceived; |
| return true; |
| } |
| /** |
| * Sets the download handler for OMA downloads, for testing purpose. |
| * |
| * @param omaDownloadHandler Download handler for OMA contents. |
| */ |
| @VisibleForTesting |
| protected void setOMADownloadHandler(OMADownloadHandler omaDownloadHandler) { |
| mOMADownloadHandler = omaDownloadHandler; |
| } |
| |
| /** See {@link DownloadManagerDelegate.EnqueueDownloadRequestTask}. */ |
| public void enqueueDownloadManagerRequest(final DownloadItem item, boolean notifyCompleted) { |
| if (mDownloadManagerRequestInterceptor != null) { |
| mDownloadManagerRequestInterceptor.interceptDownloadRequest(item, notifyCompleted); |
| return; |
| } |
| |
| mDownloadManagerDelegate.enqueueDownloadManagerRequest(item, notifyCompleted, this); |
| } |
| |
| @Override |
| public void onDownloadEnqueued( |
| boolean result, int failureReason, DownloadItem downloadItem, long downloadId) { |
| if (!result) { |
| onDownloadFailed(downloadItem.getDownloadInfo().getFileName(), failureReason); |
| recordDownloadCompletionStats( |
| true, DownloadManagerService.DOWNLOAD_STATUS_FAILED, 0, 0, 0, 0); |
| return; |
| } |
| |
| DownloadUtils.showDownloadStartToast(mContext); |
| addUmaStatsEntry(new DownloadUmaStatsEntry( |
| String.valueOf(downloadId), downloadItem.getStartTime(), 0, false, true, 0, 0)); |
| } |
| |
| /** |
| * Determines if the download should be immediately opened after |
| * downloading. |
| * |
| * @param downloadInfo Information about the download. |
| * @return true if the downloaded content should be opened, or false otherwise. |
| */ |
| @VisibleForTesting |
| static boolean shouldOpenAfterDownload(DownloadInfo downloadInfo) { |
| String type = downloadInfo.getMimeType(); |
| return downloadInfo.hasUserGesture() && MIME_TYPES_TO_OPEN.contains(type); |
| } |
| |
| /** |
| * Returns true if the download is for OMA download description file. |
| * |
| * @param downloadInfo Information about the download. |
| * @return true if the downloaded is OMA download description, or false otherwise. |
| */ |
| static boolean isOMADownloadDescription(DownloadInfo downloadInfo) { |
| return OMADownloadHandler.OMA_DOWNLOAD_DESCRIPTOR_MIME.equalsIgnoreCase( |
| downloadInfo.getMimeType()); |
| } |
| |
| /** |
| * Return the intent to launch for a given download item. |
| * |
| * @param context Context of the app. |
| * @param filePath Path to the file. |
| * @param downloadId ID of the download item in DownloadManager. |
| * @param isSupportedMimeType Whether the MIME type is supported by browser. |
| * @param downloadId ID of the download item in DownloadManager. |
| * @param originalUrl The original url of the downloaded file |
| * @param referrer Referrer of the downloaded file. |
| * @return the intent to launch for the given download item. |
| */ |
| @Nullable |
| static Intent getLaunchIntentFromDownloadId( |
| Context context, @Nullable String filePath, long downloadId, |
| boolean isSupportedMimeType, String originalUrl, String referrer) { |
| assert !ThreadUtils.runningOnUiThread(); |
| Uri contentUri = filePath == null |
| ? DownloadManagerDelegate.getContentUriFromDownloadManager(context, downloadId) |
| : ApiCompatibilityUtils.getUriForDownloadedFile(new File(filePath)); |
| if (contentUri == null) return null; |
| |
| DownloadManager manager = |
| (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); |
| String mimeType = manager.getMimeTypeForDownloadedFile(downloadId); |
| if (isSupportedMimeType) { |
| // Redirect the user to an internal media viewer. The file path is necessary to show |
| // the real file path to the user instead of a content:// download ID. |
| Uri fileUri = contentUri; |
| if (filePath != null) fileUri = Uri.fromFile(new File(filePath)); |
| return DownloadUtils.getMediaViewerIntentForDownloadItem(fileUri, contentUri, mimeType); |
| } |
| return DownloadUtils.createViewIntentForDownloadItem( |
| contentUri, mimeType, originalUrl, referrer); |
| } |
| |
| /** |
| * Return whether a download item can be resolved to any activity. |
| * |
| * @param context Context of the app. |
| * @param download A download item. |
| * @param isSupportedMimeType Whether the MIME type is supported by browser. |
| * @return true if the download item can be resolved, or false otherwise. |
| */ |
| static boolean canResolveDownloadItem(Context context, DownloadItem download, |
| boolean isSupportedMimeType) { |
| assert !ThreadUtils.runningOnUiThread(); |
| Intent intent = getLaunchIntentFromDownloadId( |
| context, download.getDownloadInfo().getFilePath(), |
| download.getSystemDownloadId(), isSupportedMimeType, null, null); |
| return (intent == null) |
| ? false : ExternalNavigationDelegateImpl.resolveIntent(intent, true); |
| } |
| |
| /** See {@link #openDownloadedContent(Context, String, boolean, boolean, String, long)}. */ |
| protected void openDownloadedContent(final DownloadInfo downloadInfo, final long downloadId) { |
| openDownloadedContent(mContext, downloadInfo.getFilePath(), |
| isSupportedMimeType(downloadInfo.getMimeType()), downloadInfo.isOffTheRecord(), |
| downloadInfo.getDownloadGuid(), downloadId, downloadInfo.getOriginalUrl(), |
| downloadInfo.getReferrer()); |
| } |
| |
| /** |
| * Launch the intent for a given download item, or Download Home if that's not possible. |
| * TODO(qinmin): Move this to DownloadManagerDelegate. |
| * |
| * @param context Context to use. |
| * @param filePath Path to the downloaded item. |
| * @param isSupportedMimeType MIME type of the downloaded item. |
| * @param isOffTheRecord Whether the download was for a off the record profile. |
| * @param downloadGuid GUID of the download item in DownloadManager. |
| * @param downloadId ID of the download item in DownloadManager. |
| * @param originalUrl The original url of the downloaded file. |
| * @param referrer Referrer of the downloaded file. |
| */ |
| protected static void openDownloadedContent(final Context context, final String filePath, |
| final boolean isSupportedMimeType, final boolean isOffTheRecord, |
| final String downloadGuid, final long downloadId, final String originalUrl, |
| final String referrer) { |
| new AsyncTask<Void, Void, Intent>() { |
| @Override |
| public Intent doInBackground(Void... params) { |
| return getLaunchIntentFromDownloadId( |
| context, filePath, downloadId, isSupportedMimeType, originalUrl, referrer); |
| } |
| |
| @Override |
| protected void onPostExecute(Intent intent) { |
| boolean didLaunchIntent = intent != null |
| && ExternalNavigationDelegateImpl.resolveIntent(intent, true) |
| && DownloadUtils.fireOpenIntentForDownload(context, intent); |
| |
| if (!didLaunchIntent) { |
| openDownloadsPage(context); |
| return; |
| } |
| |
| if (didLaunchIntent && hasDownloadManagerService()) { |
| DownloadManagerService.getDownloadManagerService().updateLastAccessTime( |
| downloadGuid, isOffTheRecord); |
| } |
| } |
| }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| |
| /** |
| * Called when a download fails. |
| * |
| * @param fileName Name of the download file. |
| * @param reason Reason of failure reported by android DownloadManager |
| */ |
| @VisibleForTesting |
| protected void onDownloadFailed(String fileName, int reason) { |
| String failureMessage = getDownloadFailureMessage(fileName, reason); |
| if (mDownloadSnackbarController.getSnackbarManager() != null) { |
| mDownloadSnackbarController.onDownloadFailed( |
| failureMessage, |
| reason == DownloadManager.ERROR_FILE_ALREADY_EXISTS); |
| } else { |
| Toast.makeText(mContext, failureMessage, Toast.LENGTH_SHORT).show(); |
| } |
| } |
| |
| /** |
| * Set the DownloadSnackbarController for testing purpose. |
| */ |
| @VisibleForTesting |
| protected void setDownloadSnackbarController( |
| DownloadSnackbarController downloadSnackbarController) { |
| mDownloadSnackbarController = downloadSnackbarController; |
| } |
| |
| /** |
| * Open the Activity which shows a list of all downloads. |
| * @param context Application context |
| */ |
| public static void openDownloadsPage(Context context) { |
| if (DownloadUtils.showDownloadManager(null, null)) return; |
| |
| // Open the Android Download Manager. |
| Intent pageView = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); |
| pageView.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| try { |
| context.startActivity(pageView); |
| } catch (ActivityNotFoundException e) { |
| Log.e(TAG, "Cannot find Downloads app", e); |
| } |
| } |
| |
| @Override |
| public void resumeDownload(ContentId id, DownloadItem item, boolean hasUserGesture) { |
| DownloadProgress progress = mDownloadProgressMap.get(item.getId()); |
| if (progress != null && progress.mDownloadStatus == DOWNLOAD_STATUS_IN_PROGRESS |
| && !progress.mDownloadItem.getDownloadInfo().isPaused()) { |
| // Download already in progress, do nothing |
| return; |
| } |
| int uma = hasUserGesture ? UMA_DOWNLOAD_RESUMPTION_CLICKED |
| : UMA_DOWNLOAD_RESUMPTION_AUTO_STARTED; |
| recordDownloadResumption(uma); |
| if (progress == null) { |
| assert !item.getDownloadInfo().isPaused(); |
| updateDownloadProgress(item, DOWNLOAD_STATUS_IN_PROGRESS); |
| progress = mDownloadProgressMap.get(item.getId()); |
| // If progress is null, the browser must have been killed while the download is active. |
| recordDownloadResumption(UMA_DOWNLOAD_RESUMPTION_BROWSER_KILLED); |
| } |
| if (hasUserGesture) { |
| // If user manually resumes a download, update the connection type that the download |
| // can start. If the previous connection type is metered, manually resuming on an |
| // unmetered network should not affect the original connection type. |
| if (!progress.mCanDownloadWhileMetered) { |
| progress.mCanDownloadWhileMetered = isActiveNetworkMetered(mContext); |
| } |
| incrementDownloadRetryCount(item.getId(), true); |
| clearDownloadRetryCount(item.getId(), true); |
| } else { |
| // TODO(qinmin): Consolidate this logic with the logic in notification service that |
| // throttles browser restarts. |
| SharedPreferences sharedPrefs = getAutoRetryCountSharedPreference(mContext); |
| int count = sharedPrefs.getInt(item.getId(), 0); |
| if (count >= getAutoResumptionLimit()) { |
| removeAutoResumableDownload(item.getId()); |
| onDownloadInterrupted(item.getDownloadInfo(), false); |
| return; |
| } |
| incrementDownloadRetryCount(item.getId(), false); |
| } |
| nativeResumeDownload(getNativeDownloadManagerService(), item.getId(), |
| item.getDownloadInfo().isOffTheRecord()); |
| } |
| |
| /** |
| * Called to cancel a download. |
| * @param id The {@link ContentId} of the download to cancel. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| @Override |
| public void cancelDownload(ContentId id, boolean isOffTheRecord) { |
| nativeCancelDownload(getNativeDownloadManagerService(), id.id, isOffTheRecord); |
| DownloadProgress progress = mDownloadProgressMap.get(id.id); |
| if (progress != null) { |
| DownloadInfo info = |
| DownloadInfo.Builder.fromDownloadInfo(progress.mDownloadItem.getDownloadInfo()) |
| .build(); |
| onDownloadCancelled(info); |
| removeDownloadProgress(id.id); |
| } |
| recordDownloadFinishedUMA(DOWNLOAD_STATUS_CANCELLED, id.id, 0); |
| } |
| |
| /** |
| * Called to pause a download. |
| * @param id The {@link ContentId} of the download to pause. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| @Override |
| public void pauseDownload(ContentId id, boolean isOffTheRecord) { |
| nativePauseDownload(getNativeDownloadManagerService(), id.id, isOffTheRecord); |
| DownloadProgress progress = mDownloadProgressMap.get(id.id); |
| // Calling pause will stop listening to the download item. Update its progress now. |
| // If download is already completed, canceled or failed, there is no need to update the |
| // download notification. |
| if (progress != null && (progress.mDownloadStatus == DOWNLOAD_STATUS_INTERRUPTED |
| || progress.mDownloadStatus == DOWNLOAD_STATUS_IN_PROGRESS)) { |
| DownloadInfo info = DownloadInfo.Builder.fromDownloadInfo( |
| progress.mDownloadItem.getDownloadInfo()).setIsPaused(true) |
| .setBytesReceived(UNKNOWN_BYTES_RECEIVED).build(); |
| onDownloadUpdated(info); |
| } |
| } |
| |
| @Override |
| public void destroyServiceDelegate() { |
| // Lifecycle of DownloadManagerService allows for this call to be ignored. |
| } |
| |
| /** |
| * Removes a download from the list. |
| * @param downloadGuid GUID of the download. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| @Override |
| public void removeDownload(final String downloadGuid, boolean isOffTheRecord) { |
| mHandler.post(() -> { |
| nativeRemoveDownload(getNativeDownloadManagerService(), downloadGuid, isOffTheRecord); |
| removeDownloadProgress(downloadGuid); |
| }); |
| |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| public Void doInBackground(Void... params) { |
| mDownloadManagerDelegate.removeCompletedDownload(downloadGuid); |
| return null; |
| } |
| }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); |
| } |
| |
| /** |
| * Checks whether the download can be opened by the browser. |
| * @param isOffTheRecord Whether the download is off the record. |
| * @param mimeType MIME type of the file. |
| * @return Whether the download is openable by the browser. |
| */ |
| @Override |
| public boolean isDownloadOpenableInBrowser(boolean isOffTheRecord, String mimeType) { |
| // TODO(qinmin): for audio and video, check if the codec is supported by Chrome. |
| return isSupportedMimeType(mimeType); |
| } |
| |
| /** |
| * Checks whether a file with the given MIME type can be opened by the browser. |
| * @param mimeType MIME type of the file. |
| * @return Whether the file would be openable by the browser. |
| */ |
| public static boolean isSupportedMimeType(String mimeType) { |
| return nativeIsSupportedMimeType(mimeType); |
| } |
| |
| /** |
| * Helper method to create and retrieve the native DownloadManagerService when needed. |
| * @return pointer to native DownloadManagerService. |
| */ |
| private long getNativeDownloadManagerService() { |
| if (mNativeDownloadManagerService == 0) { |
| mNativeDownloadManagerService = nativeInit(); |
| } |
| return mNativeDownloadManagerService; |
| } |
| |
| @CalledByNative |
| void onResumptionFailed(String downloadGuid) { |
| mDownloadNotifier.notifyDownloadFailed( |
| new DownloadInfo.Builder().setDownloadGuid(downloadGuid).build()); |
| removeDownloadProgress(downloadGuid); |
| recordDownloadResumption(UMA_DOWNLOAD_RESUMPTION_FAILED); |
| recordDownloadFinishedUMA(DOWNLOAD_STATUS_FAILED, downloadGuid, 0); |
| } |
| |
| /** |
| * Called when download success notification is shown. |
| * @param info Information about the download. |
| * @param canResolve Whether to open the download automatically. |
| * @param notificationId Notification ID of the download. |
| * @param systemDownloadId System download ID assigned by the Android DownloadManager. |
| */ |
| public void onSuccessNotificationShown( |
| DownloadInfo info, boolean canResolve, int notificationId, long systemDownloadId) { |
| if (canResolve && shouldOpenAfterDownload(info)) { |
| DownloadItem item = new DownloadItem(false, info); |
| item.setSystemDownloadId(systemDownloadId); |
| handleAutoOpenAfterDownload(item); |
| } else { |
| mDownloadSnackbarController.onDownloadSucceeded( |
| info, notificationId, systemDownloadId, canResolve, false); |
| } |
| |
| Profile profile = info.isOffTheRecord() |
| ? Profile.getLastUsedProfile().getOffTheRecordProfile() |
| : Profile.getLastUsedProfile().getOriginalProfile(); |
| Tracker tracker = TrackerFactory.getTrackerForProfile(profile); |
| tracker.notifyEvent(EventConstants.DOWNLOAD_COMPLETED); |
| } |
| |
| /** |
| * Helper method to record the download resumption UMA. |
| * @param type UMA type to be recorded. |
| */ |
| private void recordDownloadResumption(int type) { |
| assert type < UMA_DOWNLOAD_RESUMPTION_COUNT && type >= 0; |
| RecordHistogram.recordEnumeratedHistogram( |
| "MobileDownload.DownloadResumption", type, UMA_DOWNLOAD_RESUMPTION_COUNT); |
| } |
| |
| /** |
| * Helper method to record the metrics when a download completes. |
| * @param useDownloadManager Whether the download goes through Android DownloadManager. |
| * @param status Download completion status. |
| * @param totalDuration Total time in milliseconds to download the file. |
| * @param bytesDownloaded Total bytes downloaded. |
| * @param numInterruptions Number of interruptions during the download. |
| */ |
| private void recordDownloadCompletionStats(boolean useDownloadManager, int status, |
| long totalDuration, long bytesDownloaded, int numInterruptions, long bytesWasted) { |
| switch (status) { |
| case DOWNLOAD_STATUS_COMPLETE: |
| if (useDownloadManager) { |
| RecordHistogram.recordLongTimesHistogram( |
| "MobileDownload.DownloadTime.DownloadManager.Success", |
| totalDuration, TimeUnit.MILLISECONDS); |
| RecordHistogram.recordCount1000Histogram( |
| "MobileDownload.BytesDownloaded.DownloadManager.Success", |
| (int) (bytesDownloaded / 1024)); |
| } else { |
| RecordHistogram.recordLongTimesHistogram( |
| "MobileDownload.DownloadTime.ChromeNetworkStack.Success", |
| totalDuration, TimeUnit.MILLISECONDS); |
| RecordHistogram.recordCount1000Histogram( |
| "MobileDownload.BytesDownloaded.ChromeNetworkStack.Success", |
| (int) (bytesDownloaded / 1024)); |
| RecordHistogram.recordCountHistogram( |
| "MobileDownload.InterruptionsCount.ChromeNetworkStack.Success", |
| numInterruptions); |
| recordBytesWasted( |
| "MobileDownload.BytesWasted.ChromeNetworkStack.Success", bytesWasted); |
| } |
| break; |
| case DOWNLOAD_STATUS_FAILED: |
| if (useDownloadManager) { |
| RecordHistogram.recordLongTimesHistogram( |
| "MobileDownload.DownloadTime.DownloadManager.Failure", |
| totalDuration, TimeUnit.MILLISECONDS); |
| RecordHistogram.recordCount1000Histogram( |
| "MobileDownload.BytesDownloaded.DownloadManager.Failure", |
| (int) (bytesDownloaded / 1024)); |
| } else { |
| RecordHistogram.recordLongTimesHistogram( |
| "MobileDownload.DownloadTime.ChromeNetworkStack.Failure", |
| totalDuration, TimeUnit.MILLISECONDS); |
| RecordHistogram.recordCount1000Histogram( |
| "MobileDownload.BytesDownloaded.ChromeNetworkStack.Failure", |
| (int) (bytesDownloaded / 1024)); |
| RecordHistogram.recordCountHistogram( |
| "MobileDownload.InterruptionsCount.ChromeNetworkStack.Failure", |
| numInterruptions); |
| recordBytesWasted( |
| "MobileDownload.BytesWasted.ChromeNetworkStack.Failure", bytesWasted); |
| } |
| break; |
| case DOWNLOAD_STATUS_CANCELLED: |
| if (!useDownloadManager) { |
| RecordHistogram.recordLongTimesHistogram( |
| "MobileDownload.DownloadTime.ChromeNetworkStack.Cancel", |
| totalDuration, TimeUnit.MILLISECONDS); |
| RecordHistogram.recordCountHistogram( |
| "MobileDownload.InterruptionsCount.ChromeNetworkStack.Cancel", |
| numInterruptions); |
| recordBytesWasted( |
| "MobileDownload.BytesWasted.ChromeNetworkStack.Cancel", bytesWasted); |
| } |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Helper method to record the bytes wasted metrics when a download completes. |
| * @param name Histogram name |
| * @param bytesWasted Bytes wasted during download. |
| */ |
| private void recordBytesWasted(String name, long bytesWasted) { |
| RecordHistogram.recordCustomCountHistogram( |
| name, (int) (bytesWasted / 1024), 1, GB_IN_KILO_BYTES, 50); |
| } |
| |
| @Override |
| public void onQueryCompleted( |
| DownloadManagerDelegate.DownloadQueryResult result, boolean showNotification) { |
| if (result.downloadStatus == DOWNLOAD_STATUS_IN_PROGRESS) return; |
| if (showNotification) { |
| switch (result.downloadStatus) { |
| case DOWNLOAD_STATUS_COMPLETE: |
| if (shouldOpenAfterDownload(result.item.getDownloadInfo()) |
| && result.canResolve) { |
| handleAutoOpenAfterDownload(result.item); |
| } else { |
| mDownloadSnackbarController.onDownloadSucceeded( |
| result.item.getDownloadInfo(), |
| DownloadSnackbarController.INVALID_NOTIFICATION_ID, |
| result.item.getSystemDownloadId(), result.canResolve, true); |
| } |
| break; |
| case DOWNLOAD_STATUS_FAILED: |
| onDownloadFailed( |
| result.item.getDownloadInfo().getFileName(), result.failureReason); |
| break; |
| default: |
| break; |
| } |
| } |
| recordDownloadCompletionStats(true, result.downloadStatus, |
| result.downloadTimeInMilliseconds, result.bytesDownloaded, 0, 0); |
| removeUmaStatsEntry(result.item.getId()); |
| } |
| |
| /** |
| * Called by tests to disable listening to network connection changes. |
| */ |
| @VisibleForTesting |
| static void disableNetworkListenerForTest() { |
| sIsNetworkListenerDisabled = true; |
| } |
| |
| /** |
| * Called by tests to set the network type. |
| * @isNetworkMetered Whether the network should appear to be metered. |
| */ |
| @VisibleForTesting |
| static void setIsNetworkMeteredForTest(boolean isNetworkMetered) { |
| sIsNetworkMetered = isNetworkMetered; |
| } |
| |
| /** |
| * Helper method to add an auto resumable download. |
| * @param guid Id of the download item. |
| */ |
| private void addAutoResumableDownload(String guid) { |
| if (mAutoResumableDownloadIds.isEmpty() && !sIsNetworkListenerDisabled) { |
| mNetworkChangeNotifier = new NetworkChangeNotifierAutoDetect( |
| this, new RegistrationPolicyAlwaysRegister()); |
| } |
| if (!mAutoResumableDownloadIds.contains(guid)) { |
| mAutoResumableDownloadIds.add(guid); |
| } |
| } |
| |
| /** |
| * Helper method to remove an auto resumable download. |
| * @param guid Id of the download item. |
| */ |
| private void removeAutoResumableDownload(String guid) { |
| if (mAutoResumableDownloadIds.isEmpty()) return; |
| mAutoResumableDownloadIds.remove(guid); |
| stopListenToConnectionChangeIfNotNeeded(); |
| } |
| |
| /** |
| * Helper method to remove a download from |mDownloadProgressMap|. |
| * @param guid Id of the download item. |
| */ |
| private void removeDownloadProgress(String guid) { |
| mDownloadProgressMap.remove(guid); |
| removeAutoResumableDownload(guid); |
| } |
| |
| @Override |
| public void onConnectionTypeChanged(int connectionType) { |
| if (mAutoResumableDownloadIds.isEmpty()) return; |
| if (connectionType == ConnectionType.CONNECTION_NONE) return; |
| boolean isMetered = isActiveNetworkMetered(mContext); |
| // Make a copy of |mAutoResumableDownloadIds| as scheduleDownloadResumption() may delete |
| // elements inside the array. |
| List<String> copies = new ArrayList<String>(mAutoResumableDownloadIds); |
| Iterator<String> iterator = copies.iterator(); |
| while (iterator.hasNext()) { |
| final String id = iterator.next(); |
| final DownloadProgress progress = mDownloadProgressMap.get(id); |
| // Introduce some delay in each resumption so we don't start all of them immediately. |
| if (progress != null && (progress.mCanDownloadWhileMetered || !isMetered)) { |
| scheduleDownloadResumption(progress.mDownloadItem); |
| } |
| } |
| stopListenToConnectionChangeIfNotNeeded(); |
| } |
| |
| /** |
| * Helper method to stop listening to the connection type change |
| * if it is no longer needed. |
| */ |
| private void stopListenToConnectionChangeIfNotNeeded() { |
| if (mAutoResumableDownloadIds.isEmpty() && mNetworkChangeNotifier != null) { |
| mNetworkChangeNotifier.destroy(); |
| mNetworkChangeNotifier = null; |
| } |
| } |
| |
| static boolean isActiveNetworkMetered(Context context) { |
| if (sIsNetworkListenerDisabled) return sIsNetworkMetered; |
| ConnectivityManager cm = |
| (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); |
| return cm.isActiveNetworkMetered(); |
| } |
| |
| /** |
| * Adds a DownloadUmaStatsEntry to |mUmaEntries| and SharedPrefs. |
| * @param umaEntry A DownloadUmaStatsEntry to be added. |
| */ |
| private void addUmaStatsEntry(DownloadUmaStatsEntry umaEntry) { |
| mUmaEntries.add(umaEntry); |
| storeUmaEntries(); |
| } |
| |
| /** |
| * Gets a DownloadUmaStatsEntry from |mUmaEntries| given by its ID. |
| * @param id ID of the UMA entry. |
| */ |
| private DownloadUmaStatsEntry getUmaStatsEntry(String id) { |
| Iterator<DownloadUmaStatsEntry> iterator = mUmaEntries.iterator(); |
| while (iterator.hasNext()) { |
| DownloadUmaStatsEntry entry = iterator.next(); |
| if (entry.id.equals(id)) { |
| return entry; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Removes a DownloadUmaStatsEntry from SharedPrefs given by the id. |
| * @param id ID to be removed. |
| */ |
| private void removeUmaStatsEntry(String id) { |
| Iterator<DownloadUmaStatsEntry> iterator = mUmaEntries.iterator(); |
| boolean found = false; |
| while (iterator.hasNext()) { |
| DownloadUmaStatsEntry entry = iterator.next(); |
| if (entry.id.equals(id)) { |
| iterator.remove(); |
| found = true; |
| break; |
| } |
| } |
| if (found) { |
| storeUmaEntries(); |
| } |
| } |
| |
| /** |
| * Helper method to store all the DownloadUmaStatsEntry into SharedPreferences. |
| */ |
| private void storeUmaEntries() { |
| Set<String> entries = new HashSet<String>(); |
| for (int i = 0; i < mUmaEntries.size(); ++i) { |
| entries.add(mUmaEntries.get(i).getSharedPreferenceString()); |
| } |
| storeDownloadInfo(mSharedPrefs, DOWNLOAD_UMA_ENTRY, entries); |
| } |
| |
| /** |
| * Helper method to record the download completion UMA and remove the SharedPreferences entry. |
| */ |
| private void recordDownloadFinishedUMA( |
| int downloadStatus, String entryId, long bytesDownloaded) { |
| DownloadUmaStatsEntry entry = getUmaStatsEntry(entryId); |
| if (entry == null) return; |
| long currentTime = System.currentTimeMillis(); |
| long totalTime = Math.max(0, currentTime - entry.downloadStartTime); |
| recordDownloadCompletionStats( |
| false, downloadStatus, totalTime, bytesDownloaded, entry.numInterruptions, |
| entry.bytesWasted); |
| removeUmaStatsEntry(entryId); |
| } |
| |
| /** |
| * Parse the DownloadUmaStatsEntry from the shared preference. |
| */ |
| private void parseUMAStatsEntriesFromSharedPrefs() { |
| if (mSharedPrefs.contains(DOWNLOAD_UMA_ENTRY)) { |
| Set<String> entries = |
| DownloadManagerService.getStoredDownloadInfo(mSharedPrefs, DOWNLOAD_UMA_ENTRY); |
| for (String entryString : entries) { |
| DownloadUmaStatsEntry entry = DownloadUmaStatsEntry.parseFromString(entryString); |
| if (entry != null) mUmaEntries.add(entry); |
| } |
| } |
| } |
| |
| /** Adds a new DownloadHistoryAdapter to the list. */ |
| @Override |
| public void addDownloadHistoryAdapter(DownloadHistoryAdapter adapter) { |
| mHistoryAdapters.addObserver(adapter); |
| DownloadSharedPreferenceHelper.getInstance().addObserver(adapter); |
| } |
| |
| /** Removes a DownloadHistoryAdapter from the list. */ |
| @Override |
| public void removeDownloadHistoryAdapter(DownloadHistoryAdapter adapter) { |
| mHistoryAdapters.removeObserver(adapter); |
| DownloadSharedPreferenceHelper.getInstance().removeObserver(adapter); |
| } |
| |
| /** |
| * Begins sending back information about all entries in the user's DownloadHistory via |
| * {@link #onAllDownloadsRetrieved}. If the DownloadHistory is not initialized yet, the |
| * callback will be delayed. |
| * |
| * @param isOffTheRecord Whether or not to get downloads for the off the record profile. |
| */ |
| @Override |
| public void getAllDownloads(boolean isOffTheRecord) { |
| nativeGetAllDownloads(getNativeDownloadManagerService(), isOffTheRecord); |
| } |
| |
| /** |
| * Fires an Intent that alerts the DownloadNotificationService that an action must be taken |
| * for a particular item. |
| */ |
| @Override |
| public void broadcastDownloadAction(DownloadItem downloadItem, String action) { |
| if (!BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER) |
| .isStartupSuccessfullyCompleted() |
| || !ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOADS_FOREGROUND)) { |
| Intent intent = DownloadNotificationService.buildActionIntent(mContext, action, |
| LegacyHelpers.buildLegacyContentId(false, downloadItem.getId()), |
| downloadItem.getDownloadInfo().isOffTheRecord()); |
| mContext.sendBroadcast(intent); |
| } else { |
| Intent intent = DownloadNotificationFactory.buildActionIntent(mContext, action, |
| LegacyHelpers.buildLegacyContentId(false, downloadItem.getId()), |
| downloadItem.getDownloadInfo().isOffTheRecord()); |
| mContext.startService(intent); |
| } |
| } |
| |
| /** |
| * Checks if the files associated with any downloads have been removed by an external action. |
| * @param isOffTheRecord Whether or not to check downloads for the off the record profile. |
| */ |
| @Override |
| public void checkForExternallyRemovedDownloads(boolean isOffTheRecord) { |
| nativeCheckForExternallyRemovedDownloads(getNativeDownloadManagerService(), isOffTheRecord); |
| } |
| |
| @CalledByNative |
| private List<DownloadItem> createDownloadItemList() { |
| return new ArrayList<DownloadItem>(); |
| } |
| |
| @CalledByNative |
| private void addDownloadItemToList(List<DownloadItem> list, DownloadItem item) { |
| list.add(item); |
| } |
| |
| @CalledByNative |
| private void onAllDownloadsRetrieved(final List<DownloadItem> list, boolean isOffTheRecord) { |
| for (DownloadHistoryAdapter adapter : mHistoryAdapters) { |
| adapter.onAllDownloadsRetrieved(list, isOffTheRecord); |
| } |
| } |
| |
| @CalledByNative |
| private void onDownloadItemCreated(DownloadItem item) { |
| for (DownloadHistoryAdapter adapter : mHistoryAdapters) { |
| adapter.onDownloadItemCreated(item); |
| } |
| } |
| |
| @CalledByNative |
| private void onDownloadItemUpdated(DownloadItem item) { |
| for (DownloadHistoryAdapter adapter : mHistoryAdapters) { |
| adapter.onDownloadItemUpdated(item); |
| } |
| } |
| |
| @CalledByNative |
| private void onDownloadItemRemoved(String guid, boolean isOffTheRecord) { |
| for (DownloadHistoryAdapter adapter : mHistoryAdapters) { |
| adapter.onDownloadItemRemoved(guid, isOffTheRecord); |
| } |
| } |
| |
| /** |
| * Called when a download is canceled before download target is determined. |
| * |
| * @param fileName Name of the download file. |
| * @param reason Reason of failure reported by android DownloadManager. |
| */ |
| @CalledByNative |
| private static void onDownloadItemCanceled(String fileName, boolean isExternalStorageMissing) { |
| DownloadManagerService service = getDownloadManagerService(); |
| int reason = isExternalStorageMissing ? DownloadManager.ERROR_DEVICE_NOT_FOUND |
| : DownloadManager.ERROR_FILE_ALREADY_EXISTS; |
| service.onDownloadFailed(fileName, reason); |
| } |
| |
| /** |
| * Get the message to display when a download fails. |
| * |
| * @param fileName Name of the download file. |
| * @param reason Reason of failure reported by android DownloadManager. |
| */ |
| private String getDownloadFailureMessage(String fileName, int reason) { |
| switch (reason) { |
| case DownloadManager.ERROR_FILE_ALREADY_EXISTS: |
| return mContext.getString( |
| R.string.download_failed_reason_file_already_exists, fileName); |
| case DownloadManager.ERROR_FILE_ERROR: |
| return mContext.getString( |
| R.string.download_failed_reason_file_system_error, fileName); |
| case DownloadManager.ERROR_INSUFFICIENT_SPACE: |
| return mContext.getString( |
| R.string.download_failed_reason_insufficient_space, fileName); |
| case DownloadManager.ERROR_CANNOT_RESUME: |
| case DownloadManager.ERROR_HTTP_DATA_ERROR: |
| return mContext.getString( |
| R.string.download_failed_reason_network_failures, fileName); |
| case DownloadManager.ERROR_TOO_MANY_REDIRECTS: |
| case DownloadManager.ERROR_UNHANDLED_HTTP_CODE: |
| return mContext.getString( |
| R.string.download_failed_reason_server_issues, fileName); |
| case DownloadManager.ERROR_DEVICE_NOT_FOUND: |
| return mContext.getString( |
| R.string.download_failed_reason_storage_not_found, fileName); |
| case DownloadManager.ERROR_UNKNOWN: |
| default: |
| return mContext.getString( |
| R.string.download_failed_reason_unknown_error, fileName); |
| } |
| } |
| |
| /** |
| * Returns the SharedPreferences for download retry count. |
| * @return The SharedPreferences to use. |
| */ |
| private static SharedPreferences getAutoRetryCountSharedPreference(Context context) { |
| return context.getSharedPreferences(DOWNLOAD_RETRY_COUNT_FILE_NAME, Context.MODE_PRIVATE); |
| } |
| |
| /** |
| * Increments the interruption count for a download. If the interruption count reaches a certain |
| * threshold, the download will no longer auto resume unless user click the resume button to |
| * clear the count. |
| * |
| * @param downloadGuid Download GUID. |
| * @param hasUserGesture Whether the retry is caused by user gesture. |
| */ |
| private void incrementDownloadRetryCount(String downloadGuid, boolean hasUserGesture) { |
| String name = getDownloadRetryCountSharedPrefName(downloadGuid, hasUserGesture, false); |
| incrementDownloadRetrySharedPreferenceCount(name); |
| name = getDownloadRetryCountSharedPrefName(downloadGuid, hasUserGesture, true); |
| incrementDownloadRetrySharedPreferenceCount(name); |
| } |
| |
| /** |
| * Helper method to increment the retry count for a SharedPreference entry. |
| * @param sharedPreferenceName Name of the SharedPreference entry. |
| */ |
| private void incrementDownloadRetrySharedPreferenceCount(String sharedPreferenceName) { |
| SharedPreferences sharedPrefs = getAutoRetryCountSharedPreference(mContext); |
| int count = sharedPrefs.getInt(sharedPreferenceName, 0); |
| SharedPreferences.Editor editor = sharedPrefs.edit(); |
| count++; |
| editor.putInt(sharedPreferenceName, count); |
| editor.apply(); |
| } |
| |
| /** |
| * Helper method to retrieve the SharedPreference name for different download retry types. |
| * TODO(qinmin): introduce a proto for this and consolidate all the UMA metrics (including |
| * retry counts in DownloadHistory) stored in persistent storage. |
| * @param downloadGuid Guid of the download. |
| * @param hasUserGesture Whether the SharedPreference is for manual retry attempts. |
| * @param isTotalCount Whether the SharedPreference is for total retry attempts. |
| */ |
| private String getDownloadRetryCountSharedPrefName( |
| String downloadGuid, boolean hasUserGesture, boolean isTotalCount) { |
| if (isTotalCount) return downloadGuid + DOWNLOAD_TOTAL_RETRY_SUFFIX; |
| if (hasUserGesture) return downloadGuid + DOWNLOAD_MANUAL_RETRY_SUFFIX; |
| return downloadGuid; |
| } |
| |
| /** |
| * clears the retry count for a download. |
| * |
| * @param downloadGuid Download GUID. |
| * @param isAutoRetryOnly Whether to clear the auto retry count only. |
| */ |
| private void clearDownloadRetryCount(String downloadGuid, boolean isAutoRetryOnly) { |
| SharedPreferences sharedPrefs = getAutoRetryCountSharedPreference(mContext); |
| String name = getDownloadRetryCountSharedPrefName(downloadGuid, !isAutoRetryOnly, false); |
| int count = Math.min(sharedPrefs.getInt(name, 0), 200); |
| assert count >= 0; |
| SharedPreferences.Editor editor = sharedPrefs.edit(); |
| editor.remove(name); |
| if (isAutoRetryOnly) { |
| RecordHistogram.recordSparseSlowlyHistogram( |
| "MobileDownload.ResumptionsCount.Automatic", count); |
| } else { |
| RecordHistogram.recordSparseSlowlyHistogram( |
| "MobileDownload.ResumptionsCount.Manual", count); |
| name = getDownloadRetryCountSharedPrefName(downloadGuid, false, true); |
| count = sharedPrefs.getInt(name, 0); |
| assert count >= 0; |
| RecordHistogram.recordSparseSlowlyHistogram( |
| "MobileDownload.ResumptionsCount.Total", Math.min(count, 500)); |
| editor.remove(name); |
| } |
| editor.apply(); |
| } |
| |
| int getAutoResumptionLimit() { |
| if (mAutoResumptionLimit < 0) { |
| mAutoResumptionLimit = nativeGetAutoResumptionLimit(); |
| } |
| return mAutoResumptionLimit; |
| } |
| |
| /** |
| * Updates the last access time of a download. |
| * @param downloadGuid Download GUID. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| @Override |
| public void updateLastAccessTime(String downloadGuid, boolean isOffTheRecord) { |
| if (TextUtils.isEmpty(downloadGuid)) return; |
| |
| nativeUpdateLastAccessTime(getNativeDownloadManagerService(), downloadGuid, isOffTheRecord); |
| } |
| |
| @Override |
| public void onConnectionSubtypeChanged(int newConnectionSubtype) {} |
| |
| @Override |
| public void onNetworkConnect(long netId, int connectionType) {} |
| |
| @Override |
| public void onNetworkSoonToDisconnect(long netId) {} |
| |
| @Override |
| public void onNetworkDisconnect(long netId) {} |
| |
| @Override |
| public void purgeActiveNetworkList(long[] activeNetIds) {} |
| |
| private static native boolean nativeIsSupportedMimeType(String mimeType); |
| private static native int nativeGetAutoResumptionLimit(); |
| |
| private native long nativeInit(); |
| private native void nativeResumeDownload( |
| long nativeDownloadManagerService, String downloadGuid, boolean isOffTheRecord); |
| private native void nativeCancelDownload( |
| long nativeDownloadManagerService, String downloadGuid, boolean isOffTheRecord); |
| private native void nativePauseDownload(long nativeDownloadManagerService, String downloadGuid, |
| boolean isOffTheRecord); |
| private native void nativeRemoveDownload(long nativeDownloadManagerService, String downloadGuid, |
| boolean isOffTheRecord); |
| private native void nativeGetAllDownloads( |
| long nativeDownloadManagerService, boolean isOffTheRecord); |
| private native void nativeCheckForExternallyRemovedDownloads( |
| long nativeDownloadManagerService, boolean isOffTheRecord); |
| private native void nativeUpdateLastAccessTime( |
| long nativeDownloadManagerService, String downloadGuid, boolean isOffTheRecord); |
| } |