| // 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.annotation.SuppressLint; |
| import android.app.DownloadManager; |
| import android.content.ActivityNotFoundException; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.database.Cursor; |
| import android.net.ConnectivityManager; |
| import android.net.NetworkInfo; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.provider.MediaStore.MediaColumns; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.chromium.base.Callback; |
| import org.chromium.base.ContentUriUtils; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.ObserverList; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.NativeMethods; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.base.task.AsyncTask; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.download.DownloadManagerBridge.DownloadEnqueueRequest; |
| import org.chromium.chrome.browser.download.DownloadManagerBridge.DownloadEnqueueResponse; |
| import org.chromium.chrome.browser.download.DownloadNotificationUmaHelper.UmaDownloadResumption; |
| import org.chromium.chrome.browser.feature_engagement.TrackerFactory; |
| import org.chromium.chrome.browser.flags.CachedFeatureFlags; |
| import org.chromium.chrome.browser.flags.ChromeFeatureList; |
| import org.chromium.chrome.browser.media.MediaViewerUtils; |
| import org.chromium.chrome.browser.preferences.ChromePreferenceKeys; |
| import org.chromium.chrome.browser.preferences.Pref; |
| import org.chromium.chrome.browser.preferences.SharedPreferencesManager; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.profiles.ProfileKey; |
| import org.chromium.chrome.browser.profiles.ProfileManager; |
| import org.chromium.components.browser_ui.util.ConversionUtils; |
| import org.chromium.components.download.DownloadCollectionBridge; |
| import org.chromium.components.download.DownloadState; |
| import org.chromium.components.external_intents.ExternalNavigationHandler; |
| 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.LegacyHelpers; |
| import org.chromium.components.offline_items_collection.OfflineItem; |
| import org.chromium.components.offline_items_collection.OfflineItemSchedule; |
| import org.chromium.components.offline_items_collection.PendingState; |
| import org.chromium.components.prefs.PrefService; |
| import org.chromium.components.user_prefs.UserPrefs; |
| import org.chromium.content_public.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.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.RejectedExecutionException; |
| |
| /** |
| * Chrome implementation of the {@link DownloadController.Observer} 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 DownloadManagerBridge, as it |
| * handles all Android DownloadManager interactions. And DownloadManagerService should not know |
| * download Id issued by Android DownloadManager. |
| */ |
| public class DownloadManagerService implements DownloadController.Observer, |
| NetworkChangeNotifierAutoDetect.Observer, |
| DownloadServiceDelegate, ProfileManager.Observer { |
| private static final String TAG = "DownloadService"; |
| 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; |
| public static final long UNKNOWN_BYTES_RECEIVED = -1; |
| |
| private static final Set<String> sFirstSeenDownloadIds = new HashSet<String>(); |
| |
| private static DownloadManagerService sDownloadManagerService; |
| private static boolean sIsNetworkListenerDisabled; |
| private static boolean sIsNetworkMetered; |
| |
| private final SharedPreferencesManager 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; |
| |
| // Deprecated after new download backend. |
| /** Generic interface for notifying external UI components about downloads and their states. */ |
| public interface DownloadObserver extends DownloadSharedPreferenceHelper.Observer { |
| /** Called in response to {@link DownloadManagerService#getAllDownloads(boolean)}. */ |
| void onAllDownloadsRetrieved(final List<DownloadItem> list, boolean isOffTheRecord); |
| |
| /** Called when a download is created. */ |
| void onDownloadItemCreated(DownloadItem item); |
| |
| /** Called when a download is updated. */ |
| void onDownloadItemUpdated(DownloadItem item); |
| |
| /** Called when a download has been removed. */ |
| void onDownloadItemRemoved(String guid, boolean isOffTheRecord); |
| |
| /** Only for testing */ |
| default void broadcastDownloadSuccessfulForTesting(DownloadInfo downloadInfo) {} |
| } |
| |
| @VisibleForTesting protected final List<String> mAutoResumableDownloadIds = |
| new ArrayList<String>(); |
| private final ObserverList<DownloadObserver> mDownloadObservers = new ObserverList<>(); |
| |
| private OMADownloadHandler mOMADownloadHandler; |
| private DownloadSnackbarController mDownloadSnackbarController; |
| private DownloadInfoBarController mInfoBarController; |
| private DownloadInfoBarController mIncognitoInfoBarController; |
| private long mNativeDownloadManagerService; |
| 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; |
| |
| // Whether any ChromeActivity is launched. |
| private boolean mActivityLaunched; |
| |
| // Disabling call to DownloadManager.addCompletedDownload() for test. |
| private boolean mDisableAddCompletedDownloadForTesting; |
| |
| /** |
| * 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. |
| */ |
| interface DownloadManagerRequestInterceptor { |
| void interceptDownloadRequest(DownloadItem item, boolean notifyComplete); |
| } |
| |
| // Deprecated after new download backend. |
| /** |
| * Class representing progress of a download. |
| */ |
| private static class DownloadProgress { |
| final long mStartTimeInMillis; |
| boolean mCanDownloadWhileMetered; |
| DownloadItem mDownloadItem; |
| @DownloadStatus |
| int mDownloadStatus; |
| boolean mIsAutoResumable; |
| boolean mIsUpdated; |
| boolean mIsSupportedMimeType; |
| |
| DownloadProgress(long startTimeInMillis, boolean canDownloadWhileMetered, |
| DownloadItem downloadItem, @DownloadStatus 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(); |
| if (sDownloadManagerService == null) { |
| DownloadNotifier downloadNotifier = new SystemDownloadNotifier(); |
| sDownloadManagerService = new DownloadManagerService( |
| 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( |
| DownloadNotifier downloadNotifier, Handler handler, long updateDelayInMillis) { |
| Context applicationContext = ContextUtils.getApplicationContext(); |
| mSharedPrefs = SharedPreferencesManager.getInstance(); |
| mDownloadNotifier = downloadNotifier; |
| mUpdateDelayInMillis = updateDelayInMillis; |
| mHandler = handler; |
| mDownloadSnackbarController = new DownloadSnackbarController(); |
| mOMADownloadHandler = new OMADownloadHandler(applicationContext); |
| DownloadCollectionBridge.setDownloadDelegate(new DownloadDelegateImpl()); |
| // 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); |
| // Clean up unused shared prefs. TODO(qinmin): remove this after M84. |
| mSharedPrefs.removeKey(ChromePreferenceKeys.DOWNLOAD_UMA_ENTRY); |
| } |
| |
| // TODO(https://crbug.com/1060940): Remove this function and update all use cases so that |
| // the profile would be available instead of isOffTheRecord boolean. |
| private static ProfileKey getProfileKey(boolean isOffTheRecord) { |
| // If off-the-record is not requested, the request might be before native initialization. |
| if (!isOffTheRecord) return ProfileKey.getLastUsedRegularProfileKey(); |
| |
| return Profile.getLastUsedRegularProfile().getPrimaryOTRProfile().getProfileKey(); |
| } |
| |
| /** |
| * Initializes download related systems for background task. |
| */ |
| public void initForBackgroundTask() { |
| getNativeDownloadManagerService(); |
| } |
| |
| /** |
| * Pre-load shared prefs to avoid being blocked on the disk access async task in the future. |
| */ |
| public static void warmUpSharedPrefs() { |
| getAutoRetryCountSharedPreference(); |
| } |
| |
| public DownloadNotifier getDownloadNotifier() { |
| return mDownloadNotifier; |
| } |
| |
| /** @return The {@link DownloadInfoBarController} controller associated with the profile. */ |
| public DownloadInfoBarController getInfoBarController(boolean isIncognito) { |
| return isIncognito ? mIncognitoInfoBarController : mInfoBarController; |
| } |
| |
| /** For testing only. */ |
| public void setInfoBarControllerForTesting(DownloadInfoBarController infoBarController) { |
| mInfoBarController = infoBarController; |
| } |
| |
| // Deprecated after new download backend. |
| @Override |
| public void onDownloadCompleted(final DownloadInfo downloadInfo) { |
| @DownloadStatus |
| int status = DownloadStatus.COMPLETE; |
| String mimeType = downloadInfo.getMimeType(); |
| if (downloadInfo.getBytesReceived() == 0) { |
| status = DownloadStatus.FAILED; |
| } else { |
| mimeType = MimeUtils.remapGenericMimeType( |
| mimeType, downloadInfo.getOriginalUrl(), downloadInfo.getFileName()); |
| } |
| DownloadInfo newInfo = |
| DownloadInfo.Builder.fromDownloadInfo(downloadInfo).setMimeType(mimeType).build(); |
| DownloadItem downloadItem = new DownloadItem(false, newInfo); |
| downloadItem.setSystemDownloadId( |
| DownloadManagerBridge.getDownloadIdForDownloadGuid(downloadInfo.getDownloadGuid())); |
| updateDownloadProgress(downloadItem, status); |
| updateDownloadInfoBar(downloadItem); |
| } |
| |
| // Deprecated after new download backend. |
| @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, DownloadStatus.IN_PROGRESS); |
| updateDownloadInfoBar(item); |
| scheduleUpdateIfNeeded(); |
| } |
| |
| // Deprecated after new download backend. |
| @Override |
| public void onDownloadCancelled(final DownloadInfo downloadInfo) { |
| DownloadInfo newInfo = DownloadInfo.Builder.fromDownloadInfo(downloadInfo) |
| .setState(DownloadState.CANCELLED) |
| .build(); |
| DownloadItem item = new DownloadItem(false, newInfo); |
| removeAutoResumableDownload(item.getId()); |
| updateDownloadProgress(new DownloadItem(false, downloadInfo), DownloadStatus.CANCELLED); |
| updateDownloadInfoBar(item); |
| } |
| |
| // Deprecated after new download backend. |
| @Override |
| public void onDownloadInterrupted(final DownloadInfo downloadInfo, boolean isAutoResumable) { |
| @DownloadStatus |
| int status = DownloadStatus.INTERRUPTED; |
| DownloadItem item = new DownloadItem(false, downloadInfo); |
| if (!downloadInfo.isResumable()) { |
| status = DownloadStatus.FAILED; |
| } else if (isAutoResumable) { |
| addAutoResumableDownload(item.getId()); |
| } |
| |
| updateDownloadProgress(item, status); |
| updateDownloadInfoBar(item); |
| |
| if (CachedFeatureFlags.isEnabled(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE)) { |
| return; |
| } |
| DownloadProgress progress = mDownloadProgressMap.get(item.getId()); |
| if (progress == null) return; |
| if (!isAutoResumable || sIsNetworkListenerDisabled) return; |
| ConnectivityManager cm = |
| (ConnectivityManager) ContextUtils.getApplicationContext().getSystemService( |
| Context.CONNECTIVITY_SERVICE); |
| NetworkInfo info = cm.getActiveNetworkInfo(); |
| if (info == null || !info.isConnected()) return; |
| if (progress.mCanDownloadWhileMetered |
| || !isActiveNetworkMetered(ContextUtils.getApplicationContext())) { |
| // 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); |
| } |
| } |
| |
| // Deprecated after native auto-resumption. |
| /** |
| * 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() { |
| if (!mActivityLaunched) { |
| mInfoBarController = new DownloadInfoBarController(false); |
| mIncognitoInfoBarController = new DownloadInfoBarController(true); |
| |
| DownloadNotificationService.clearResumptionAttemptLeft(); |
| |
| DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads( |
| /*isOffTheRecord=*/false); |
| mActivityLaunched = true; |
| } |
| } |
| |
| private void updateDownloadInfoBar(DownloadItem item) { |
| DownloadInfoBarController infobarController = |
| getInfoBarController(item.getDownloadInfo().isOffTheRecord()); |
| if (infobarController != null) infobarController.onDownloadItemUpdated(item); |
| } |
| |
| /** |
| * Broadcast that a download was successful. |
| * @param downloadInfo info about the download. |
| */ |
| // For testing only. |
| protected void broadcastDownloadSuccessful(DownloadInfo downloadInfo) { |
| for (DownloadObserver observer : mDownloadObservers) { |
| observer.broadcastDownloadSuccessfulForTesting(downloadInfo); |
| } |
| } |
| |
| /** |
| * Gets download information from SharedPreferences. |
| * @param sharedPrefs The SharedPreferencesManager to read from. |
| * @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( |
| SharedPreferencesManager sharedPrefs, String type) { |
| return new HashSet<>(sharedPrefs.readStringSet(type)); |
| } |
| |
| /** |
| * Stores download information to shared preferences. The information can be |
| * either pending download IDs, or pending OMA downloads. |
| * |
| * @param sharedPrefs SharedPreferencesManager to write to. |
| * @param type Type of the information. |
| * @param downloadInfo Information to be saved. |
| * @param forceCommit Whether to synchronously update shared preferences. |
| */ |
| @SuppressLint({"ApplySharedPref", "CommitPrefEdits"}) |
| static void storeDownloadInfo(SharedPreferencesManager sharedPrefs, String type, |
| Set<String> downloadInfo, boolean forceCommit) { |
| boolean success; |
| if (downloadInfo.isEmpty()) { |
| if (forceCommit) { |
| success = sharedPrefs.removeKeySync(type); |
| } else { |
| sharedPrefs.removeKey(type); |
| success = true; |
| } |
| } else { |
| if (forceCommit) { |
| success = sharedPrefs.writeStringSetSync(type, downloadInfo); |
| } else { |
| sharedPrefs.writeStringSet(type, downloadInfo); |
| success = true; |
| } |
| } |
| |
| if (!success) { |
| // Write synchronously because it might be used on restart and needs to stay |
| // up-to-date. |
| Log.e(TAG, "Failed to write DownloadInfo " + type); |
| } |
| } |
| |
| /** |
| * 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)); |
| } |
| } |
| |
| // Deprecated after new download backend. |
| /** |
| * 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 DownloadStatus.COMPLETE: |
| notificationUpdateScheduled = updateDownloadSuccessNotification(progress); |
| removeFromDownloadProgressMap = notificationUpdateScheduled; |
| break; |
| case DownloadStatus.FAILED: |
| // TODO(cmsy): Use correct FailState. |
| mDownloadNotifier.notifyDownloadFailed(info); |
| Log.w(TAG, "Download failed: " + info.getFilePath()); |
| break; |
| case DownloadStatus.IN_PROGRESS: |
| if (info.isPaused()) { |
| mDownloadNotifier.notifyDownloadPaused(info); |
| DownloadNotificationUmaHelper.recordDownloadResumptionHistogram( |
| UmaDownloadResumption.MANUAL_PAUSE); |
| } else { |
| mDownloadNotifier.notifyDownloadProgress( |
| info, progress.mStartTimeInMillis, progress.mCanDownloadWhileMetered); |
| removeFromDownloadProgressMap = false; |
| } |
| break; |
| case DownloadStatus.CANCELLED: |
| mDownloadNotifier.notifyDownloadCanceled(item.getContentId()); |
| break; |
| case DownloadStatus.INTERRUPTED: |
| mDownloadNotifier.notifyDownloadInterrupted( |
| info, progress.mIsAutoResumable, PendingState.PENDING_NETWORK); |
| removeFromDownloadProgressMap = !progress.mIsAutoResumable; |
| break; |
| default: |
| assert false; |
| break; |
| } |
| if (notificationUpdateScheduled) progress.mIsUpdated = false; |
| if (removeFromDownloadProgressMap) mDownloadProgressMap.remove(item.getId()); |
| } |
| |
| // Deprecated after new download backend. |
| /** |
| * 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; |
| final DownloadInfo info = item.getDownloadInfo(); |
| |
| AsyncTask<Pair<Boolean, Boolean>> task = new AsyncTask<Pair<Boolean, Boolean>>() { |
| @Override |
| public Pair<Boolean, Boolean> doInBackground() { |
| boolean success = mDisableAddCompletedDownloadForTesting |
| || ContentUriUtils.isContentUri(item.getDownloadInfo().getFilePath()); |
| boolean shouldAddCompletedDownload = |
| !ChromeFeatureList.isEnabled( |
| ChromeFeatureList.DOWNLOAD_OFFLINE_CONTENT_PROVIDER) |
| && (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q); |
| if (!success && shouldAddCompletedDownload) { |
| long systemDownloadId = DownloadManagerBridge.addCompletedDownload( |
| info.getFileName(), info.getDescription(), info.getMimeType(), |
| info.getFilePath(), info.getBytesReceived(), info.getOriginalUrl(), |
| info.getReferrer(), info.getDownloadGuid()); |
| success = systemDownloadId != DownloadConstants.INVALID_DOWNLOAD_ID; |
| if (success) item.setSystemDownloadId(systemDownloadId); |
| } |
| boolean canResolve = success |
| && (MimeUtils.isOMADownloadDescription(item.getDownloadInfo().getMimeType()) |
| || canResolveDownloadItem(item, isSupportedMimeType)); |
| return Pair.create(success, canResolve); |
| } |
| |
| @Override |
| protected void onPostExecute(Pair<Boolean, Boolean> result) { |
| DownloadInfo info = item.getDownloadInfo(); |
| if (result.first) { |
| mDownloadNotifier.notifyDownloadSuccessful( |
| info, item.getSystemDownloadId(), result.second, isSupportedMimeType); |
| broadcastDownloadSuccessful(info); |
| } else { |
| info = DownloadInfo.Builder.fromDownloadInfo(info) |
| .setFailState(FailState.CANNOT_DOWNLOAD) |
| .build(); |
| mDownloadNotifier.notifyDownloadFailed(info); |
| // TODO(qinmin): get the failure message from native. |
| } |
| } |
| }; |
| 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; |
| } |
| } |
| |
| @CalledByNative |
| private void handleOMADownload(DownloadItem download, long systemDownloadId) { |
| mOMADownloadHandler.handleOMADownload(download.getDownloadInfo(), systemDownloadId); |
| } |
| |
| /** |
| * Handle auto opennable files after download completes. |
| * TODO(qinmin): move this to DownloadManagerBridge. |
| * |
| * @param download A download item. |
| */ |
| private void handleAutoOpenAfterDownload(DownloadItem download) { |
| if (MimeUtils.isOMADownloadDescription(download.getDownloadInfo().getMimeType())) { |
| mOMADownloadHandler.handleOMADownload( |
| download.getDownloadInfo(), download.getSystemDownloadId()); |
| return; |
| } |
| openDownloadedContent(download.getDownloadInfo(), download.getSystemDownloadId(), |
| DownloadOpenSource.AUTO_OPEN); |
| } |
| |
| // Deprecated after new download backend. |
| /** |
| * 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. |
| */ |
| // Deprecated after new download backend. |
| private void updateDownloadProgress( |
| DownloadItem downloadItem, @DownloadStatus int downloadStatus) { |
| boolean isSupportedMimeType = downloadStatus == DownloadStatus.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(ContextUtils.getApplicationContext()), downloadItem, |
| downloadStatus); |
| progress.mIsUpdated = true; |
| progress.mIsSupportedMimeType = isSupportedMimeType; |
| mDownloadProgressMap.put(id, progress); |
| sFirstSeenDownloadIds.add(id); |
| |
| // This is mostly for testing, when the download is not tracked/progress is null but |
| // downloadStatus is not DownloadStatus.IN_PROGRESS. |
| if (downloadStatus != DownloadStatus.IN_PROGRESS) { |
| updateNotification(progress); |
| } |
| } |
| return; |
| } |
| |
| progress.mDownloadStatus = downloadStatus; |
| progress.mDownloadItem = downloadItem; |
| progress.mIsUpdated = true; |
| progress.mIsAutoResumable = mAutoResumableDownloadIds.contains(id); |
| progress.mIsSupportedMimeType = isSupportedMimeType; |
| switch (downloadStatus) { |
| case DownloadStatus.COMPLETE: |
| case DownloadStatus.FAILED: |
| case DownloadStatus.CANCELLED: |
| clearDownloadRetryCount(id, true); |
| clearDownloadRetryCount(id, false); |
| updateNotification(progress); |
| sFirstSeenDownloadIds.remove(id); |
| break; |
| case DownloadStatus.INTERRUPTED: |
| updateNotification(progress); |
| break; |
| case DownloadStatus.IN_PROGRESS: |
| if (downloadItem.getDownloadInfo().isPaused()) { |
| updateNotification(progress); |
| } |
| break; |
| default: |
| assert false; |
| } |
| } |
| |
| /** See {@link DownloadManagerBridge.enqueueNewDownload}. */ |
| public void enqueueNewDownload(final DownloadItem item, boolean notifyCompleted) { |
| if (mDownloadManagerRequestInterceptor != null) { |
| mDownloadManagerRequestInterceptor.interceptDownloadRequest(item, notifyCompleted); |
| return; |
| } |
| |
| DownloadEnqueueRequest request = new DownloadEnqueueRequest(); |
| request.url = item.getDownloadInfo().getUrl(); |
| request.fileName = item.getDownloadInfo().getFileName(); |
| request.description = item.getDownloadInfo().getDescription(); |
| request.mimeType = item.getDownloadInfo().getMimeType(); |
| request.cookie = item.getDownloadInfo().getCookie(); |
| request.referrer = item.getDownloadInfo().getReferrer(); |
| request.userAgent = item.getDownloadInfo().getUserAgent(); |
| request.notifyCompleted = notifyCompleted; |
| DownloadManagerBridge.enqueueNewDownload( |
| request, response -> { onDownloadEnqueued(item, response); }); |
| } |
| |
| public void onDownloadEnqueued(DownloadItem downloadItem, DownloadEnqueueResponse response) { |
| downloadItem.setStartTime(response.startTime); |
| downloadItem.setSystemDownloadId(response.downloadId); |
| if (!response.result) { |
| onDownloadFailed(downloadItem, response.failureReason); |
| return; |
| } |
| |
| getInfoBarController(downloadItem.getDownloadInfo().isOffTheRecord()).onDownloadStarted(); |
| } |
| |
| @Nullable |
| static Intent getLaunchIntentForDownload(@Nullable String filePath, long downloadId, |
| boolean isSupportedMimeType, String originalUrl, String referrer, |
| @Nullable String mimeType) { |
| assert !ThreadUtils.runningOnUiThread(); |
| if (downloadId == DownloadConstants.INVALID_DOWNLOAD_ID) { |
| if (!ContentUriUtils.isContentUri(filePath)) return null; |
| return getLaunchIntentFromDownloadUri( |
| filePath, isSupportedMimeType, originalUrl, referrer, mimeType); |
| } |
| |
| DownloadManagerBridge.DownloadQueryResult queryResult = |
| DownloadManagerBridge.queryDownloadResult(downloadId); |
| if (mimeType == null) mimeType = queryResult.mimeType; |
| |
| Uri contentUri = filePath == null ? queryResult.contentUri |
| : DownloadUtils.getUriForOtherApps(filePath); |
| if (contentUri == null || Uri.EMPTY.equals(contentUri)) return null; |
| |
| Uri fileUri = filePath == null ? contentUri : Uri.fromFile(new File(filePath)); |
| return createLaunchIntent( |
| fileUri, contentUri, mimeType, isSupportedMimeType, originalUrl, referrer); |
| } |
| |
| /** |
| * Similar to getLaunchIntentForDownload(), but only works for download that is stored as a |
| * content Uri. |
| * @param context Context of the app. |
| * @param contentUri Uri of the download. |
| * @param isSupportedMimeType Whether the MIME type is supported by browser. |
| * @param originalUrl The original url of the downloaded file |
| * @param referrer Referrer of the downloaded file. |
| * @param mimeType MIME type of the downloaded file. |
| * @return the intent to launch for the given download item. |
| */ |
| @Nullable |
| private static Intent getLaunchIntentFromDownloadUri(String contentUri, |
| boolean isSupportedMimeType, String originalUrl, String referrer, |
| @Nullable String mimeType) { |
| assert !ThreadUtils.runningOnUiThread(); |
| assert ContentUriUtils.isContentUri(contentUri); |
| |
| Uri uri = Uri.parse(contentUri); |
| if (mimeType == null) { |
| try (Cursor cursor = ContextUtils.getApplicationContext().getContentResolver().query( |
| uri, null, null, null, null)) { |
| if (cursor == null || cursor.getCount() == 0) return null; |
| cursor.moveToNext(); |
| mimeType = cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)); |
| cursor.close(); |
| } |
| } |
| return createLaunchIntent(uri, uri, mimeType, isSupportedMimeType, originalUrl, referrer); |
| } |
| |
| /** |
| * Creates a an intent to launch a download. |
| * @param fileUri File uri of the download has an actual file path. Otherwise, this is the same |
| * as |contentUri|. |
| * @param contentUri Content uri of the download. |
| * @param isSupportedMimeType Whether the MIME type is supported by browser. |
| * @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. |
| */ |
| private static Intent createLaunchIntent(Uri fileUri, Uri contentUri, String mimeType, |
| boolean isSupportedMimeType, String originalUrl, String referrer) { |
| 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. |
| return MediaViewerUtils.getMediaViewerIntent( |
| fileUri, contentUri, mimeType, true /* allowExternalAppHandlers */); |
| } |
| return MediaViewerUtils.createViewIntentForUri(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(DownloadItem download, boolean isSupportedMimeType) { |
| assert !ThreadUtils.runningOnUiThread(); |
| Intent intent = getLaunchIntentForDownload(download.getDownloadInfo().getFilePath(), |
| download.getSystemDownloadId(), isSupportedMimeType, null, null, |
| download.getDownloadInfo().getMimeType()); |
| return (intent == null) ? false : ExternalNavigationHandler.resolveIntent(intent, true); |
| } |
| |
| /** See {@link #openDownloadedContent(Context, String, boolean, boolean, String, long)}. */ |
| protected void openDownloadedContent(final DownloadInfo downloadInfo, final long downloadId, |
| @DownloadOpenSource int source) { |
| openDownloadedContent(ContextUtils.getApplicationContext(), downloadInfo.getFilePath(), |
| isSupportedMimeType(downloadInfo.getMimeType()), downloadInfo.isOffTheRecord(), |
| downloadInfo.getDownloadGuid(), downloadId, downloadInfo.getOriginalUrl(), |
| downloadInfo.getReferrer(), source, downloadInfo.getMimeType()); |
| } |
| |
| /** |
| * Launch the intent for a given download item, or Download Home if that's not possible. |
| * TODO(qinmin): Move this to DownloadManagerBridge. |
| * |
| * @param context Context to use. |
| * @param filePath Path to the downloaded item. |
| * @param isSupportedMimeType Whether the MIME type is supported by Chrome. |
| * @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. |
| * @param source The source that tries to open the download. |
| * @param mimeType MIME type of the download, could be null. |
| */ |
| 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, @DownloadOpenSource int source, @Nullable String mimeType) { |
| new AsyncTask<Intent>() { |
| @Override |
| public Intent doInBackground() { |
| return getLaunchIntentForDownload( |
| filePath, downloadId, isSupportedMimeType, originalUrl, referrer, mimeType); |
| } |
| |
| @Override |
| protected void onPostExecute(Intent intent) { |
| boolean didLaunchIntent = intent != null |
| && ExternalNavigationHandler.resolveIntent(intent, true) |
| && DownloadUtils.fireOpenIntentForDownload(context, intent); |
| |
| if (!didLaunchIntent) { |
| openDownloadsPage(context, source); |
| return; |
| } |
| |
| if (didLaunchIntent && hasDownloadManagerService()) { |
| DownloadManagerService.getDownloadManagerService().updateLastAccessTime( |
| downloadGuid, isOffTheRecord); |
| DownloadManager manager = |
| (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); |
| String mimeType = manager.getMimeTypeForDownloadedFile(downloadId); |
| DownloadMetrics.recordDownloadOpen(source, mimeType); |
| } |
| } |
| } |
| .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(DownloadItem item, int reason) { |
| String failureMessage = |
| getDownloadFailureMessage(item.getDownloadInfo().getFileName(), reason); |
| |
| if (mDownloadSnackbarController.getSnackbarManager() != null) { |
| mDownloadSnackbarController.onDownloadFailed(failureMessage, |
| reason == DownloadManager.ERROR_FILE_ALREADY_EXISTS, |
| item.getDownloadInfo().isOffTheRecord()); |
| } else { |
| Toast.makeText(ContextUtils.getApplicationContext(), failureMessage, Toast.LENGTH_SHORT) |
| .show(); |
| } |
| } |
| |
| /** |
| * Open the Activity which shows a list of all downloads. |
| * @param context Application context |
| * @param source The source where the user action coming from. |
| */ |
| public static void openDownloadsPage(Context context, @DownloadOpenSource int source) { |
| if (DownloadUtils.showDownloadManager(null, null, source)) 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); |
| } |
| } |
| |
| // Deprecated after new download backend. |
| @Override |
| public void resumeDownload(ContentId id, DownloadItem item, boolean hasUserGesture) { |
| DownloadProgress progress = mDownloadProgressMap.get(item.getId()); |
| if (progress != null && progress.mDownloadStatus == DownloadStatus.IN_PROGRESS |
| && !progress.mDownloadItem.getDownloadInfo().isPaused()) { |
| // Download already in progress, do nothing |
| return; |
| } |
| int uma = |
| hasUserGesture ? UmaDownloadResumption.CLICKED : UmaDownloadResumption.AUTO_STARTED; |
| DownloadNotificationUmaHelper.recordDownloadResumptionHistogram(uma); |
| if (progress == null) { |
| assert !item.getDownloadInfo().isPaused(); |
| // If the download was not resumed before, the browser must have been killed while the |
| // download is active. |
| if (!sFirstSeenDownloadIds.contains(item.getId())) { |
| sFirstSeenDownloadIds.add(item.getId()); |
| DownloadNotificationUmaHelper.recordDownloadResumptionHistogram( |
| UmaDownloadResumption.BROWSER_KILLED); |
| } |
| updateDownloadProgress(item, DownloadStatus.IN_PROGRESS); |
| progress = mDownloadProgressMap.get(item.getId()); |
| } |
| 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(ContextUtils.getApplicationContext()); |
| } |
| 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(); |
| int count = sharedPrefs.getInt(item.getId(), 0); |
| if (count >= getAutoResumptionLimit()) { |
| removeAutoResumableDownload(item.getId()); |
| onDownloadInterrupted(item.getDownloadInfo(), false); |
| return; |
| } |
| incrementDownloadRetryCount(item.getId(), false); |
| } |
| |
| // Downloads started from incognito mode should not be resumed in reduced mode. |
| if (!ProfileManager.isInitialized() && item.getDownloadInfo().isOffTheRecord()) return; |
| |
| DownloadManagerServiceJni.get().resumeDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, item.getId(), |
| getProfileKey(item.getDownloadInfo().isOffTheRecord()), hasUserGesture); |
| } |
| |
| /** |
| * Called to retry a download. May resume the current download or start a new one. |
| * @param id The {@link ContentId} of the download to retry. |
| * @param item The current download that needs to retry. |
| * @param hasUserGesture Whether the request was originated due to user gesture. |
| */ |
| // Deprecated after new download backend. |
| // TODO(shaktisahu): Add retry to offline content provider or route it from resume call. |
| public void retryDownload(ContentId id, DownloadItem item, boolean hasUserGesture) { |
| DownloadManagerServiceJni.get().retryDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, item.getId(), |
| getProfileKey(item.getDownloadInfo().isOffTheRecord()), hasUserGesture); |
| } |
| |
| /** |
| * Called to cancel a download. |
| * @param id The {@link ContentId} of the download to cancel. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| // Deprecated after new download backend. |
| @Override |
| public void cancelDownload(ContentId id, boolean isOffTheRecord) { |
| DownloadManagerServiceJni.get().cancelDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, id.id, getProfileKey(isOffTheRecord)); |
| DownloadProgress progress = mDownloadProgressMap.get(id.id); |
| if (progress != null) { |
| DownloadInfo info = |
| DownloadInfo.Builder.fromDownloadInfo(progress.mDownloadItem.getDownloadInfo()) |
| .build(); |
| onDownloadCancelled(info); |
| removeDownloadProgress(id.id); |
| } else { |
| mDownloadNotifier.notifyDownloadCanceled(id); |
| DownloadInfoBarController infoBarController = getInfoBarController(isOffTheRecord); |
| if (infoBarController != null) infoBarController.onDownloadItemRemoved(id); |
| } |
| } |
| |
| /** |
| * Called to pause a download. |
| * @param id The {@link ContentId} of the download to pause. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| // Deprecated after new download backend. |
| @Override |
| public void pauseDownload(ContentId id, boolean isOffTheRecord) { |
| DownloadManagerServiceJni.get().pauseDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, id.id, getProfileKey(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 == DownloadStatus.INTERRUPTED |
| || progress.mDownloadStatus == DownloadStatus.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. |
| * @param externallyRemoved If the file is externally removed by other applications. |
| */ |
| public void removeDownload( |
| final String downloadGuid, boolean isOffTheRecord, boolean externallyRemoved) { |
| mHandler.post(() -> { |
| DownloadManagerServiceJni.get().removeDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, downloadGuid, getProfileKey(isOffTheRecord)); |
| removeDownloadProgress(downloadGuid); |
| }); |
| |
| if (ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_OFFLINE_CONTENT_PROVIDER)) { |
| return; |
| } |
| |
| DownloadManagerBridge.removeCompletedDownload(downloadGuid, externallyRemoved); |
| } |
| |
| /** |
| * 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. |
| */ |
| 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 DownloadManagerServiceJni.get().isSupportedMimeType(mimeType); |
| } |
| |
| /** |
| * Helper method to create and retrieve the native DownloadManagerService when needed. |
| * @return pointer to native DownloadManagerService. |
| */ |
| private long getNativeDownloadManagerService() { |
| if (mNativeDownloadManagerService == 0) { |
| boolean startupCompleted = ProfileManager.isInitialized(); |
| mNativeDownloadManagerService = DownloadManagerServiceJni.get().init( |
| DownloadManagerService.this, startupCompleted); |
| if (!startupCompleted) ProfileManager.addObserver(this); |
| } |
| return mNativeDownloadManagerService; |
| } |
| |
| @Override |
| public void onProfileAdded(Profile profile) { |
| ProfileManager.removeObserver(this); |
| DownloadManagerServiceJni.get().onProfileAdded( |
| mNativeDownloadManagerService, DownloadManagerService.this, profile); |
| } |
| |
| @Override |
| public void onProfileDestroyed(Profile profile) {} |
| |
| @CalledByNative |
| void onResumptionFailed(String downloadGuid) { |
| mDownloadNotifier.notifyDownloadFailed(new DownloadInfo.Builder() |
| .setDownloadGuid(downloadGuid) |
| .setFailState(FailState.CANNOT_DOWNLOAD) |
| .build()); |
| removeDownloadProgress(downloadGuid); |
| DownloadNotificationUmaHelper.recordDownloadResumptionHistogram( |
| UmaDownloadResumption.FAILED); |
| } |
| |
| /** |
| * 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 (!ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_OFFLINE_CONTENT_PROVIDER)) { |
| if (canResolve && MimeUtils.canAutoOpenMimeType(info.getMimeType()) |
| && info.hasUserGesture()) { |
| DownloadItem item = new DownloadItem(false, info); |
| item.setSystemDownloadId(systemDownloadId); |
| handleAutoOpenAfterDownload(item); |
| } else { |
| DownloadInfoBarController infobarController = |
| getInfoBarController(info.isOffTheRecord()); |
| if (infobarController != null) { |
| infobarController.onNotificationShown(info.getContentId(), notificationId); |
| } |
| } |
| } else { |
| if (getInfoBarController(info.isOffTheRecord()) != null) { |
| getInfoBarController(info.isOffTheRecord()) |
| .onNotificationShown(info.getContentId(), notificationId); |
| } |
| } |
| |
| if (BrowserStartupController.getInstance().isFullBrowserStarted()) { |
| Profile profile = info.isOffTheRecord() |
| ? Profile.getLastUsedRegularProfile().getPrimaryOTRProfile() |
| : Profile.getLastUsedRegularProfile(); |
| Tracker tracker = TrackerFactory.getTrackerForProfile(profile); |
| tracker.notifyEvent(EventConstants.DOWNLOAD_COMPLETED); |
| } |
| } |
| |
| /** |
| * 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) ConversionUtils.bytesToKilobytes(bytesWasted), 1, |
| ConversionUtils.KILOBYTES_PER_GIGABYTE, 50); |
| } |
| |
| /** |
| * Used only for android DownloadManager associated downloads. |
| * @param item The associated download item. |
| * @param showNotification Whether to show notification for this download. |
| * @param result The query result about the download. |
| */ |
| public void onQueryCompleted(DownloadItem item, boolean showNotification, |
| DownloadManagerBridge.DownloadQueryResult result) { |
| DownloadInfo.Builder builder = item.getDownloadInfo() == null |
| ? new DownloadInfo.Builder() |
| : DownloadInfo.Builder.fromDownloadInfo(item.getDownloadInfo()); |
| builder.setBytesTotalSize(result.bytesTotal); |
| builder.setBytesReceived(result.bytesDownloaded); |
| if (!TextUtils.isEmpty(result.fileName)) builder.setFileName(result.fileName); |
| if (!TextUtils.isEmpty(result.mimeType)) builder.setMimeType(result.mimeType); |
| builder.setFilePath(result.filePath); |
| item.setDownloadInfo(builder.build()); |
| |
| if (result.downloadStatus == DownloadStatus.IN_PROGRESS) return; |
| if (showNotification) { |
| switch (result.downloadStatus) { |
| case DownloadStatus.COMPLETE: |
| new AsyncTask<Boolean>() { |
| @Override |
| protected Boolean doInBackground() { |
| return canResolveDownloadItem(item, |
| isSupportedMimeType(item.getDownloadInfo().getMimeType())); |
| } |
| |
| @Override |
| protected void onPostExecute(Boolean canResolve) { |
| if (MimeUtils.canAutoOpenMimeType(result.mimeType) |
| && item.getDownloadInfo().hasUserGesture() && canResolve) { |
| handleAutoOpenAfterDownload(item); |
| } else { |
| getInfoBarController(item.getDownloadInfo().isOffTheRecord()) |
| .onItemUpdated(DownloadItem.createOfflineItem(item), null); |
| } |
| } |
| }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| break; |
| case DownloadStatus.FAILED: |
| onDownloadFailed(item, result.failureReason); |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| // Deprecated after native auto-resumption handler. |
| private void addAutoResumableDownload(String guid) { |
| if (CachedFeatureFlags.isEnabled(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE)) { |
| return; |
| } |
| 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. |
| */ |
| // Deprecated after native auto-resumption. |
| private void removeAutoResumableDownload(String guid) { |
| if (CachedFeatureFlags.isEnabled(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE)) { |
| return; |
| } |
| if (mAutoResumableDownloadIds.isEmpty()) return; |
| mAutoResumableDownloadIds.remove(guid); |
| stopListenToConnectionChangeIfNotNeeded(); |
| } |
| |
| /** |
| * Helper method to remove a download from |mDownloadProgressMap|. |
| * @param guid Id of the download item. |
| */ |
| // Deprecated after new download backend. |
| private void removeDownloadProgress(String guid) { |
| mDownloadProgressMap.remove(guid); |
| removeAutoResumableDownload(guid); |
| sFirstSeenDownloadIds.remove(guid); |
| } |
| |
| // Deprecated after native auto resumption. |
| @Override |
| public void onConnectionTypeChanged(int connectionType) { |
| if (CachedFeatureFlags.isEnabled(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE)) { |
| return; |
| } |
| if (mAutoResumableDownloadIds.isEmpty()) return; |
| if (connectionType == ConnectionType.CONNECTION_NONE) return; |
| boolean isMetered = isActiveNetworkMetered(ContextUtils.getApplicationContext()); |
| // 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. |
| */ |
| // Deprecated after native auto resumption. |
| private void stopListenToConnectionChangeIfNotNeeded() { |
| if (mAutoResumableDownloadIds.isEmpty() && mNetworkChangeNotifier != null) { |
| mNetworkChangeNotifier.destroy(); |
| mNetworkChangeNotifier = null; |
| } |
| } |
| |
| // Deprecated after native auto resumption. |
| static boolean isActiveNetworkMetered(Context context) { |
| if (sIsNetworkListenerDisabled) return sIsNetworkMetered; |
| ConnectivityManager cm = |
| (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); |
| return cm.isActiveNetworkMetered(); |
| } |
| |
| /** Adds a new DownloadObserver to the list. */ |
| // Deprecated after new download backend. |
| public void addDownloadObserver(DownloadObserver observer) { |
| mDownloadObservers.addObserver(observer); |
| DownloadSharedPreferenceHelper.getInstance().addObserver(observer); |
| } |
| |
| /** Removes a DownloadObserver from the list. */ |
| // Deprecated after new download backend. |
| public void removeDownloadObserver(DownloadObserver observer) { |
| mDownloadObservers.removeObserver(observer); |
| DownloadSharedPreferenceHelper.getInstance().removeObserver(observer); |
| } |
| |
| /** |
| * 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. |
| */ |
| // Deprecated after new download backend. |
| public void getAllDownloads(boolean isOffTheRecord) { |
| DownloadManagerServiceJni.get().getAllDownloads(getNativeDownloadManagerService(), |
| DownloadManagerService.this, getProfileKey(isOffTheRecord)); |
| } |
| |
| /** |
| * Fires an Intent that alerts the DownloadNotificationService that an action must be taken |
| * for a particular item. |
| */ |
| // Deprecated after new download backend. |
| public void broadcastDownloadAction(DownloadItem downloadItem, String action) { |
| Context appContext = ContextUtils.getApplicationContext(); |
| Intent intent = DownloadNotificationFactory.buildActionIntent(appContext, action, |
| LegacyHelpers.buildLegacyContentId(false, downloadItem.getId()), |
| downloadItem.getDownloadInfo().isOffTheRecord()); |
| addCancelExtra(intent, downloadItem); |
| appContext.startService(intent); |
| } |
| |
| // Deprecated after new download backend. |
| public void renameDownload(ContentId id, String name, |
| Callback<Integer /*RenameResult*/> callback, boolean isOffTheRecord) { |
| DownloadManagerServiceJni.get().renameDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, id.id, name, callback, getProfileKey(isOffTheRecord)); |
| } |
| |
| /** |
| * Change the download schedule to start the download in a different condition. |
| * @param id The id of the {@link OfflineItem} that requests the change. |
| * @param schedule The download schedule that defines when to start the download. |
| * @param isOffTheRecord Whether the download is for off the record profile. |
| */ |
| // Deprecated after new download backend. |
| public void changeSchedule( |
| final ContentId id, final OfflineItemSchedule schedule, boolean isOffTheRecord) { |
| boolean onlyOnWifi = (schedule == null) ? false : schedule.onlyOnWifi; |
| long startTimeMs = (schedule == null) ? -1 : schedule.startTimeMs; |
| DownloadManagerServiceJni.get().changeSchedule(getNativeDownloadManagerService(), |
| DownloadManagerService.this, id.id, onlyOnWifi, startTimeMs, |
| getProfileKey(isOffTheRecord)); |
| } |
| |
| /** |
| * Add an Intent extra for StateAtCancel UMA to know the state of a request prior to a |
| * user-initated cancel. |
| * @param intent The Intent associated with the download action. |
| * @param downloadItem The download associated with download action. |
| */ |
| // Deprecated after new download backend. |
| private void addCancelExtra(Intent intent, DownloadItem downloadItem) { |
| if (intent.getAction().equals(DownloadNotificationService.ACTION_DOWNLOAD_CANCEL)) { |
| int state; |
| if (DownloadUtils.isDownloadPaused(downloadItem)) { |
| state = DownloadNotificationUmaHelper.StateAtCancel.PAUSED; |
| } else if (DownloadUtils.isDownloadPending(downloadItem)) { |
| if (downloadItem.getDownloadInfo().getPendingState() |
| == PendingState.PENDING_NETWORK) { |
| state = DownloadNotificationUmaHelper.StateAtCancel.PENDING_NETWORK; |
| } else { |
| state = DownloadNotificationUmaHelper.StateAtCancel.PENDING_ANOTHER_DOWNLOAD; |
| } |
| } else { |
| state = DownloadNotificationUmaHelper.StateAtCancel.DOWNLOADING; |
| } |
| intent.putExtra(DownloadNotificationService.EXTRA_DOWNLOAD_STATE_AT_CANCEL, state); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| public void checkForExternallyRemovedDownloads(boolean isOffTheRecord) { |
| DownloadManagerServiceJni.get().checkForExternallyRemovedDownloads( |
| getNativeDownloadManagerService(), DownloadManagerService.this, |
| getProfileKey(isOffTheRecord)); |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private List<DownloadItem> createDownloadItemList() { |
| return new ArrayList<DownloadItem>(); |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private void addDownloadItemToList(List<DownloadItem> list, DownloadItem item) { |
| list.add(item); |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private void onAllDownloadsRetrieved(final List<DownloadItem> list, ProfileKey profileKey) { |
| // TODO(https://crbug.com/1099577): Pass the profileKey/profile to adapter instead of the |
| // boolean. |
| boolean isOffTheRecord = profileKey.isOffTheRecord(); |
| for (DownloadObserver adapter : mDownloadObservers) { |
| adapter.onAllDownloadsRetrieved(list, isOffTheRecord); |
| } |
| maybeShowMissingSdCardError(list); |
| } |
| |
| /** |
| * Shows a snackbar that tells the user that files may be missing because no SD card was found |
| * in the case that the error was not shown before and at least one of the items was |
| * externally removed and has a path that points to a missing external drive. |
| * |
| * @param list List of DownloadItems to check. |
| */ |
| // TODO(shaktisahu): Drive this from a similar observer. |
| private void maybeShowMissingSdCardError(List<DownloadItem> list) { |
| PrefService prefService = UserPrefs.get(Profile.getLastUsedRegularProfile()); |
| // Only show the missing directory snackbar once. |
| if (!prefService.getBoolean(Pref.SHOW_MISSING_SD_CARD_ERROR_ANDROID)) return; |
| |
| DownloadDirectoryProvider provider = DownloadDirectoryProvider.getInstance(); |
| provider.getAllDirectoriesOptions((ArrayList<DirectoryOption> dirs) -> { |
| if (dirs.size() > 1) return; |
| String externalStorageDir = provider.getExternalStorageDirectory(); |
| |
| for (DownloadItem item : list) { |
| boolean missingOnSDCard = isFilePathOnMissingExternalDrive( |
| item.getDownloadInfo().getFilePath(), externalStorageDir, dirs); |
| if (!isUnresumableOrCancelled(item) && missingOnSDCard) { |
| mHandler.post(() -> { |
| // TODO(shaktisahu): Show it on infobar in the right way. |
| mDownloadSnackbarController.onDownloadDirectoryNotFound(); |
| }); |
| prefService.setBoolean(Pref.SHOW_MISSING_SD_CARD_ERROR_ANDROID, false); |
| break; |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Checks to see if the item is either unresumable or cancelled. |
| * |
| * @param downloadItem Item to check. |
| * @return Whether the item is unresumable or cancelled. |
| */ |
| private boolean isUnresumableOrCancelled(DownloadItem downloadItem) { |
| @DownloadState |
| int state = downloadItem.getDownloadInfo().state(); |
| return (state == DownloadState.INTERRUPTED && !downloadItem.getDownloadInfo().isResumable()) |
| || state == DownloadState.CANCELLED; |
| } |
| |
| /** |
| * Returns whether a given file path is in a directory that is no longer available, most likely |
| * because it is on an SD card that was removed. |
| * |
| * @param filePath The file path to check, can be a content URI. |
| * @param externalStorageDir The absolute path of external storage directory for primary |
| * storage. |
| * @param directoryOptions All available download directories including primary storage and |
| * secondary storage. |
| * |
| * @return Whether this file path is in a directory that is no longer available. |
| */ |
| private boolean isFilePathOnMissingExternalDrive(String filePath, String externalStorageDir, |
| ArrayList<DirectoryOption> directoryOptions) { |
| if (TextUtils.isEmpty(filePath) || filePath.contains(externalStorageDir) |
| || ContentUriUtils.isContentUri(filePath)) { |
| return false; |
| } |
| |
| for (DirectoryOption directory : directoryOptions) { |
| if (TextUtils.isEmpty(directory.location)) continue; |
| if (filePath.contains(directory.location)) return false; |
| } |
| |
| return true; |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private void onDownloadItemCreated(DownloadItem item) { |
| for (DownloadObserver adapter : mDownloadObservers) { |
| adapter.onDownloadItemCreated(item); |
| } |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private void onDownloadItemUpdated(DownloadItem item) { |
| for (DownloadObserver adapter : mDownloadObservers) { |
| adapter.onDownloadItemUpdated(item); |
| } |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private void onDownloadItemRemoved(String guid, boolean isOffTheRecord) { |
| DownloadInfoBarController infobarController = getInfoBarController(isOffTheRecord); |
| if (infobarController != null) { |
| infobarController.onDownloadItemRemoved( |
| LegacyHelpers.buildLegacyContentId(false, guid)); |
| } |
| |
| for (DownloadObserver adapter : mDownloadObservers) { |
| adapter.onDownloadItemRemoved(guid, isOffTheRecord); |
| } |
| } |
| |
| // Deprecated after new download backend. |
| @CalledByNative |
| private void openDownloadItem(DownloadItem downloadItem, @DownloadOpenSource int source) { |
| DownloadInfo downloadInfo = downloadItem.getDownloadInfo(); |
| boolean canOpen = |
| DownloadUtils.openFile(downloadInfo.getFilePath(), downloadInfo.getMimeType(), |
| downloadInfo.getDownloadGuid(), downloadInfo.isOffTheRecord(), |
| downloadInfo.getOriginalUrl(), downloadInfo.getReferrer(), source); |
| if (!canOpen) { |
| openDownloadsPage(ContextUtils.getApplicationContext(), source); |
| } |
| } |
| |
| /** |
| * Opens a download. If the download cannot be opened, download home will be opened instead. |
| * @param id The {@link ContentId} of the download to be opened. |
| * @param source The source where the user opened this download. |
| */ |
| // Deprecated after new download backend. |
| public void openDownload(ContentId id, boolean isOffTheRecord, @DownloadOpenSource int source) { |
| DownloadManagerServiceJni.get().openDownload(getNativeDownloadManagerService(), |
| DownloadManagerService.this, id.id, getProfileKey(isOffTheRecord), source); |
| } |
| |
| /** |
| * Checks whether the download will be immediately opened after completion. |
| * @param downloadItem The download item to be opened. |
| * @return True if the download will be auto-opened, false otherwise. |
| */ |
| public void checkIfDownloadWillAutoOpen(DownloadItem downloadItem, Callback<Boolean> callback) { |
| assert(downloadItem.getDownloadInfo().state() == DownloadState.COMPLETE); |
| |
| AsyncTask<Boolean> task = new AsyncTask<Boolean>() { |
| @Override |
| public Boolean doInBackground() { |
| DownloadInfo info = downloadItem.getDownloadInfo(); |
| boolean isSupportedMimeType = isSupportedMimeType(info.getMimeType()); |
| boolean canResolve = MimeUtils.isOMADownloadDescription(info.getMimeType()) |
| || canResolveDownloadItem(downloadItem, isSupportedMimeType); |
| return canResolve && MimeUtils.canAutoOpenMimeType(info.getMimeType()) |
| && info.hasUserGesture(); |
| } |
| @Override |
| protected void onPostExecute(Boolean result) { |
| callback.onResult(result); |
| } |
| }; |
| |
| try { |
| task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } catch (RejectedExecutionException e) { |
| // Reaching thread limit, update will be reschduled for the next run. |
| Log.e(TAG, "Thread limit reached, reschedule notification update later."); |
| } |
| } |
| |
| /** |
| * Called when a download is canceled before download target is determined. |
| * |
| * @param item The download item. |
| * @param isExternalStorageMissing Whether the reason for failure is missing external storage. |
| */ |
| @CalledByNative |
| private static void onDownloadItemCanceled( |
| DownloadItem item, boolean isExternalStorageMissing) { |
| DownloadManagerService service = getDownloadManagerService(); |
| int reason = isExternalStorageMissing ? DownloadManager.ERROR_DEVICE_NOT_FOUND |
| : DownloadManager.ERROR_FILE_ALREADY_EXISTS; |
| service.onDownloadFailed(item, reason); |
| |
| // TODO(shaktisahu): Notify infobar controller. |
| } |
| |
| /** |
| * 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) { |
| Context appContext = ContextUtils.getApplicationContext(); |
| switch (reason) { |
| case DownloadManager.ERROR_FILE_ALREADY_EXISTS: |
| return appContext.getString( |
| R.string.download_failed_reason_file_already_exists, fileName); |
| case DownloadManager.ERROR_FILE_ERROR: |
| return appContext.getString( |
| R.string.download_failed_reason_file_system_error, fileName); |
| case DownloadManager.ERROR_INSUFFICIENT_SPACE: |
| return appContext.getString( |
| R.string.download_failed_reason_insufficient_space, fileName); |
| case DownloadManager.ERROR_CANNOT_RESUME: |
| case DownloadManager.ERROR_HTTP_DATA_ERROR: |
| return appContext.getString( |
| R.string.download_failed_reason_network_failures, fileName); |
| case DownloadManager.ERROR_TOO_MANY_REDIRECTS: |
| case DownloadManager.ERROR_UNHANDLED_HTTP_CODE: |
| return appContext.getString( |
| R.string.download_failed_reason_server_issues, fileName); |
| case DownloadManager.ERROR_DEVICE_NOT_FOUND: |
| return appContext.getString( |
| R.string.download_failed_reason_storage_not_found, fileName); |
| case DownloadManager.ERROR_UNKNOWN: |
| default: |
| return appContext.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() { |
| return ContextUtils.getApplicationContext().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. |
| */ |
| // Deprecated after new download backend. |
| 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. |
| */ |
| // Deprecated after new download backend. |
| private void incrementDownloadRetrySharedPreferenceCount(String sharedPreferenceName) { |
| SharedPreferences sharedPrefs = getAutoRetryCountSharedPreference(); |
| 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. |
| */ |
| // Deprecated after new download backend. |
| 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. |
| */ |
| // Deprecated after new download backend. |
| private void clearDownloadRetryCount(String downloadGuid, boolean isAutoRetryOnly) { |
| SharedPreferences sharedPrefs = getAutoRetryCountSharedPreference(); |
| 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.recordSparseHistogram( |
| "MobileDownload.ResumptionsCount.Automatic", count); |
| } else { |
| RecordHistogram.recordSparseHistogram("MobileDownload.ResumptionsCount.Manual", count); |
| name = getDownloadRetryCountSharedPrefName(downloadGuid, false, true); |
| count = sharedPrefs.getInt(name, 0); |
| assert count >= 0; |
| RecordHistogram.recordSparseHistogram( |
| "MobileDownload.ResumptionsCount.Total", Math.min(count, 500)); |
| editor.remove(name); |
| } |
| editor.apply(); |
| } |
| |
| // Deprecated after new download backend. |
| int getAutoResumptionLimit() { |
| if (mAutoResumptionLimit < 0) { |
| mAutoResumptionLimit = DownloadManagerServiceJni.get().getAutoResumptionLimit(); |
| } |
| return mAutoResumptionLimit; |
| } |
| |
| /** |
| * Creates an interrupted download in native code to be used by instrumentation tests. |
| * @param url URL of the download. |
| * @param guid Download GUID. |
| * @param targetPath Target file path. |
| */ |
| void createInterruptedDownloadForTest(String url, String guid, String targetPath) { |
| DownloadManagerServiceJni.get().createInterruptedDownloadForTest( |
| getNativeDownloadManagerService(), DownloadManagerService.this, url, guid, |
| targetPath); |
| } |
| |
| void disableAddCompletedDownloadToDownloadManager() { |
| mDisableAddCompletedDownloadForTesting = true; |
| } |
| |
| /** |
| * Updates the last access time of a download. |
| * @param downloadGuid Download GUID. |
| * @param isOffTheRecord Whether the download is off the record. |
| */ |
| // Deprecated after new download backend. |
| public void updateLastAccessTime(String downloadGuid, boolean isOffTheRecord) { |
| if (TextUtils.isEmpty(downloadGuid)) return; |
| |
| DownloadManagerServiceJni.get().updateLastAccessTime(getNativeDownloadManagerService(), |
| DownloadManagerService.this, downloadGuid, getProfileKey(isOffTheRecord)); |
| } |
| |
| // Deprecated after native auto-resumption handler. |
| @Override |
| public void onConnectionSubtypeChanged(int newConnectionSubtype) {} |
| |
| // Deprecated after native auto-resumption handler. |
| @Override |
| public void onNetworkConnect(long netId, int connectionType) {} |
| |
| // Deprecated after native auto-resumption handler. |
| @Override |
| public void onNetworkSoonToDisconnect(long netId) {} |
| |
| // Deprecated after native auto-resumption handler. |
| @Override |
| public void onNetworkDisconnect(long netId) {} |
| |
| // Deprecated after native auto-resumption handler. |
| @Override |
| public void purgeActiveNetworkList(long[] activeNetIds) {} |
| |
| @NativeMethods |
| interface Natives { |
| boolean isSupportedMimeType(String mimeType); |
| int getAutoResumptionLimit(); |
| long init(DownloadManagerService caller, boolean isProfileAdded); |
| void openDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey, int source); |
| void resumeDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey, boolean hasUserGesture); |
| void retryDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey, boolean hasUserGesture); |
| void cancelDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey); |
| void pauseDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey); |
| void removeDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey); |
| void renameDownload(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, String targetName, Callback</*RenameResult*/ Integer> callback, |
| ProfileKey profileKey); |
| void changeSchedule(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, boolean onlyOnWifi, long startTimeMs, ProfileKey profileKey); |
| void getAllDownloads(long nativeDownloadManagerService, DownloadManagerService caller, |
| ProfileKey profileKey); |
| void checkForExternallyRemovedDownloads(long nativeDownloadManagerService, |
| DownloadManagerService caller, ProfileKey profileKey); |
| void updateLastAccessTime(long nativeDownloadManagerService, DownloadManagerService caller, |
| String downloadGuid, ProfileKey profileKey); |
| void onProfileAdded( |
| long nativeDownloadManagerService, DownloadManagerService caller, Profile profile); |
| void createInterruptedDownloadForTest(long nativeDownloadManagerService, |
| DownloadManagerService caller, String url, String guid, String targetPath); |
| } |
| } |