// 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.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; import android.os.Handler; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.LongSparseArray; import android.util.Pair; 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.annotations.SuppressFBWarnings; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.R; 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.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 extends BroadcastReceiver implements DownloadController.DownloadNotificationService, NetworkChangeNotifierAutoDetect.Observer, DownloadManagerDelegate.DownloadQueryCallback, 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"; protected static final String PENDING_OMA_DOWNLOADS = "PendingOMADownloads"; private static final String UNKNOWN_MIME_TYPE = "application/unknown"; private static final String DOWNLOAD_UMA_ENTRY = "DownloadUmaEntry"; 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; // 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; // 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; private final LongSparseArray<DownloadItem> mSystemDownloadIdMap = new LongSparseArray<DownloadItem>(); @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; /** * 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; } } /** * Class representing an OMA download entry to be stored in SharedPrefs. * TODO(qinmin): Move all OMA related class and functions to a separate class. */ @VisibleForTesting protected static class OMAEntry { final long mDownloadId; final String mInstallNotifyURI; OMAEntry(long downloadId, String installNotifyURI) { mDownloadId = downloadId; mInstallNotifyURI = installNotifyURI; } /** * Parse OMA entry from the SharedPrefs String * TODO(qinmin): use a file instead of SharedPrefs to store the OMA entry. * * @param entry String contains the OMA information. * @return an OMAEntry object. */ @VisibleForTesting static OMAEntry parseOMAEntry(String entry) { int index = entry.indexOf(","); long downloadId = Long.parseLong(entry.substring(0, index)); return new OMAEntry(downloadId, entry.substring(index + 1)); } /** * Generates a string for an OMA entry to be inserted into the SharedPrefs. * TODO(qinmin): use a file instead of SharedPrefs to store the OMA entry. * * @return a String representing the download entry. */ String generateSharedPrefsString() { return String.valueOf(mDownloadId) + "," + mInstallNotifyURI; } } /** * Creates DownloadManagerService. */ @SuppressFBWarnings("LI_LAZY_INIT") // Findbugs doesn't see this is only UI thread. public static DownloadManagerService getDownloadManagerService(final Context context) { ThreadUtils.assertOnUiThread(); assert context == context.getApplicationContext(); if (sDownloadManagerService == null) { sDownloadManagerService = new DownloadManagerService(context, new SystemDownloadNotifier(context), 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 protected DownloadManagerService(Context context, DownloadNotifier downloadNotifier, Handler handler, long updateDelayInMillis) { mContext = context; mSharedPrefs = ContextUtils.getAppSharedPreferences(); mDownloadNotifier = downloadNotifier; mUpdateDelayInMillis = updateDelayInMillis; mHandler = handler; mOMADownloadHandler = new OMADownloadHandler(context); mDownloadSnackbarController = new DownloadSnackbarController(context); mDownloadManagerDelegate = new DownloadManagerDelegate(mContext); // 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(); clearPendingOMADownloads(); } @VisibleForTesting protected void init() { DownloadController.setDownloadNotificationService(this); // Post a delayed task to resume all pending downloads. mHandler.postDelayed(new Runnable() { @Override public void run() { 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(); } } public DownloadNotifier getDownloadNotifier() { return mDownloadNotifier; } @Override public void onDownloadCompleted(final DownloadInfo downloadInfo) { int status = DOWNLOAD_STATUS_COMPLETE; String mimeType = downloadInfo.getMimeType(); if (downloadInfo.getContentLength() == 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); scheduleUpdateIfNeeded(); } @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); scheduleUpdateIfNeeded(); } @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); scheduleUpdateIfNeeded(); } /** * Called when browser activity is launched. For background resumption and cancellation, this * will not be called. */ public void onActivityLaunched() { DownloadNotificationService.clearResumptionAttemptLeft(); } /** * Clear any pending OMA downloads by reading them from shared prefs. * TODO(qinmin): move this to a separate class. */ public void clearPendingOMADownloads() { if (mSharedPrefs.contains(PENDING_OMA_DOWNLOADS)) { Set<String> omaDownloads = getStoredDownloadInfo(mSharedPrefs, PENDING_OMA_DOWNLOADS); for (String omaDownload : omaDownloads) { OMAEntry entry = OMAEntry.parseOMAEntry(omaDownload); clearPendingOMADownload(entry.mDownloadId, entry.mInstallNotifyURI); } } } /** * Async task to clear the pending OMA download from SharedPrefs and inform * the OMADownloadHandler about download status. * TODO(qinmin): move this to a separate file. */ private class ClearPendingOMADownloadTask extends AsyncTask<Void, Void, Pair<Integer, Boolean>> { private final DownloadItem mDownloadItem; private final String mInstallNotifyURI; private DownloadInfo mDownloadInfo; private int mFailureReason; public ClearPendingOMADownloadTask(DownloadItem downloadItem, String installNotifyURI) { mDownloadItem = downloadItem; mInstallNotifyURI = installNotifyURI; mDownloadInfo = downloadItem.getDownloadInfo(); } @Override public Pair<Integer, Boolean> doInBackground(Void...voids) { DownloadManager manager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); Cursor c = manager.query(new DownloadManager.Query().setFilterById( mDownloadItem.getSystemDownloadId())); int statusIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS); int reasonIndex = c.getColumnIndex(DownloadManager.COLUMN_REASON); int titleIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE); int status = DownloadManager.STATUS_FAILED; Boolean canResolve = false; if (c.moveToNext()) { status = c.getInt(statusIndex); String title = c.getString(titleIndex); if (mDownloadInfo == null) { // Chrome has been killed, reconstruct a DownloadInfo. mDownloadInfo = new DownloadInfo.Builder() .setFileName(title) .setDescription(c.getString( c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION))) .setMimeType(c.getString( c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE))) .setContentLength(Long.parseLong(c.getString( c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)))) .build(); } if (status == DownloadManager.STATUS_SUCCESSFUL) { mDownloadInfo = DownloadInfo.Builder.fromDownloadInfo(mDownloadInfo) .setFileName(title) .build(); mDownloadItem.setDownloadInfo(mDownloadInfo); canResolve = canResolveDownloadItem(mContext, mDownloadItem, false); } else if (status == DownloadManager.STATUS_FAILED) { mFailureReason = c.getInt(reasonIndex); manager.remove(mDownloadItem.getSystemDownloadId()); } } c.close(); return Pair.create(status, canResolve); } @Override protected void onPostExecute(Pair<Integer, Boolean> result) { long downloadId = mDownloadItem.getSystemDownloadId(); if (result.first == DownloadManager.STATUS_SUCCESSFUL) { mOMADownloadHandler.onDownloadCompleted( mDownloadInfo, downloadId, mInstallNotifyURI); removeOMADownloadFromSharedPrefs(downloadId); mDownloadSnackbarController.onDownloadSucceeded( mDownloadInfo, DownloadSnackbarController.INVALID_NOTIFICATION_ID, downloadId, result.second); } else if (result.first == DownloadManager.STATUS_FAILED) { mOMADownloadHandler.onDownloadFailed( mDownloadInfo, downloadId, mFailureReason, mInstallNotifyURI); removeOMADownloadFromSharedPrefs(downloadId); String fileName = mDownloadInfo.getFileName(); onDownloadFailed(fileName, mFailureReason); } } } /** * Clear pending OMA downloads for a particular download ID. * * @param downloadId Download identifier from Android DownloadManager. * @param installNotifyURI URI to notify after installation. */ private void clearPendingOMADownload(long downloadId, String installNotifyURI) { DownloadItem item = mSystemDownloadIdMap.get(downloadId); if (item == null) { item = new DownloadItem(true, null); item.setSystemDownloadId(downloadId); } ClearPendingOMADownloadTask task = new ClearPendingOMADownloadTask(item, installNotifyURI); task.execute(); } /** * 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>())); } /** * Add OMA download info to SharedPrefs. * @param omaInfo OMA download information to save. */ @VisibleForTesting protected void addOMADownloadToSharedPrefs(String omaInfo) { Set<String> omaDownloads = getStoredDownloadInfo(mSharedPrefs, PENDING_OMA_DOWNLOADS); omaDownloads.add(omaInfo); storeDownloadInfo(mSharedPrefs, PENDING_OMA_DOWNLOADS, omaDownloads); } /** * Remove OMA download info from SharedPrefs. * @param downloadId ID to be removed. */ private void removeOMADownloadFromSharedPrefs(long downloadId) { Set<String> omaDownloads = getStoredDownloadInfo(mSharedPrefs, PENDING_OMA_DOWNLOADS); for (String omaDownload : omaDownloads) { OMAEntry entry = OMAEntry.parseOMAEntry(omaDownload); if (entry.mDownloadId == downloadId) { omaDownloads.remove(omaDownload); storeDownloadInfo(mSharedPrefs, PENDING_OMA_DOWNLOADS, omaDownloads); return; } } } /** * Check if a download ID is in OMA SharedPrefs. * @param downloadId Download identifier to check. * @param true if it is in the SharedPrefs, or false otherwise. */ private boolean isDownloadIdInOMASharedPrefs(long downloadId) { Set<String> omaDownloads = getStoredDownloadInfo(mSharedPrefs, PENDING_OMA_DOWNLOADS); for (String omaDownload : omaDownloads) { OMAEntry entry = OMAEntry.parseOMAEntry(omaDownload); if (entry.mDownloadId == downloadId) { return true; } } return false; } /** * 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. Should not be called from UI thread. * @param progresses A list of notifications to update. * @return A List of failed downloads. */ private List<DownloadItem> updateAllNotifications(List<DownloadProgress> progresses) { assert !ThreadUtils.runningOnUiThread(); List<DownloadItem> downloadItems = new ArrayList<DownloadItem>(); for (int i = 0; i < progresses.size(); ++i) { DownloadProgress progress = progresses.get(i); DownloadItem item = progress.mDownloadItem; DownloadInfo info = item.getDownloadInfo(); switch (progress.mDownloadStatus) { case DOWNLOAD_STATUS_COMPLETE: boolean success = addCompletedDownload(item); if (success) { boolean canResolve = isOMADownloadDescription(info) || canResolveDownloadItem( mContext, item, progress.mIsSupportedMimeType); long systemDownloadId = item.getSystemDownloadId(); mDownloadNotifier.notifyDownloadSuccessful( info, systemDownloadId, canResolve, progress.mIsSupportedMimeType); broadcastDownloadSuccessful(info); } else { downloadItems.add(item); mDownloadNotifier.notifyDownloadFailed(info); } break; case DOWNLOAD_STATUS_FAILED: downloadItems.add(item); mDownloadNotifier.notifyDownloadFailed(info); Log.w(TAG, "Download failed: " + info.getFilePath()); 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); } break; case DOWNLOAD_STATUS_CANCELLED: mDownloadNotifier.notifyDownloadCanceled(item.getId()); break; case DOWNLOAD_STATUS_INTERRUPTED: mDownloadNotifier.notifyDownloadInterrupted(info, progress.mIsAutoResumable); break; default: assert false; break; } } return downloadItems; } /** * 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) { 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.getContentLength(), downloadInfo.getOriginalUrl(), downloadInfo.getReferer(), 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. */ private 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; } // Make a copy of the |progressUpdated|, so that we can update the notification on another // thread without worrying about concurrent modifications. final List<DownloadProgress> progressToUpdate = new ArrayList<DownloadProgress>(); for (int i = 0; i < progressPendingUpdate.size(); ++i) { progressToUpdate.add(new DownloadProgress(progressPendingUpdate.get(i))); } AsyncTask<Void, Void, List<DownloadItem>> task = new AsyncTask<Void, Void, List<DownloadItem>>() { @Override public List<DownloadItem> doInBackground(Void... params) { return updateAllNotifications(progressToUpdate); } @Override protected void onPostExecute(List<DownloadItem> result) { for (int i = 0; i < result.size(); ++i) { // TODO(qinmin): get the failure message from native. onDownloadFailed(result.get(i).getDownloadInfo().getFileName(), DownloadManager.ERROR_UNKNOWN); } } }; try { task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); for (int i = 0; i < progressPendingUpdate.size(); ++i) { DownloadProgress progress = progressPendingUpdate.get(i); progress.mIsUpdated = false; // Remove progress entry from mDownloadProgressMap if they are no longer needed. if ((progress.mDownloadStatus != DOWNLOAD_STATUS_IN_PROGRESS || progress.mDownloadItem.getDownloadInfo().isPaused()) && (progress.mDownloadStatus != DOWNLOAD_STATUS_INTERRUPTED || !progress.mIsAutoResumable)) { mDownloadProgressMap.remove(progress.mDownloadItem.getId()); } } } catch (RejectedExecutionException e) { // Reaching thread limit, update will be reschduled for the next run. Log.e(TAG, "reaching thread limit, reschedule notification update later."); } Runnable scheduleNextUpdateTask = new Runnable(){ @Override public void run() { 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()) : false; String id = downloadItem.getId(); DownloadProgress progress = mDownloadProgressMap.get(id); 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); if (getUmaStatsEntry(downloadItem.getId()) == null) { addUmaStatsEntry(new DownloadUmaStatsEntry( downloadItem.getId(), startTime, 0, false, false)); } } 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().getContentLength()); break; case DOWNLOAD_STATUS_INTERRUPTED: entry = getUmaStatsEntry(downloadItem.getId()); entry.numInterruptions++; storeUmaEntries(); break; case DOWNLOAD_STATUS_IN_PROGRESS: entry = getUmaStatsEntry(downloadItem.getId()); if (entry.isPaused != downloadItem.getDownloadInfo().isPaused()) { entry.isPaused = downloadItem.getDownloadInfo().isPaused(); storeUmaEntries(); } break; default: assert false; } } /** * 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; } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) return; long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, DownloadItem.INVALID_DOWNLOAD_ID); if (downloadId == DownloadItem.INVALID_DOWNLOAD_ID) return; boolean isPendingOMADownload = mOMADownloadHandler.isPendingOMADownload(downloadId); boolean isInOMASharedPrefs = isDownloadIdInOMASharedPrefs(downloadId); if (isPendingOMADownload || isInOMASharedPrefs) { clearPendingOMADownload(downloadId, null); mSystemDownloadIdMap.remove(downloadId); return; } DownloadItem downloadItem = mSystemDownloadIdMap.get(downloadId); if (downloadItem != null) { mDownloadManagerDelegate.queryDownloadResult(downloadItem, true, this); mSystemDownloadIdMap.remove(downloadId); if (mSystemDownloadIdMap.size() == 0) { mContext.unregisterReceiver(this); } } } /** * Sends the download request to Android download manager. If |notifyCompleted| is true, * a notification will be sent to the user once download is complete and the downloaded * content will be saved to the public directory on external storage. Otherwise, the * download will be saved in the app directory and user will not get any notifications * after download completion. * This will be used by OMA downloads as we need Android DownloadManager to encrypt the content. * TODO(qinmin): move this to DownloadManagerDelegate. * * @param info Download information about the download. * @param notifyCompleted Whether to notify the user after Downloadmanager completes the * download. */ public void enqueueDownloadManagerRequest( final DownloadItem item, boolean notifyCompleted) { EnqueueDownloadRequestTask task = new EnqueueDownloadRequestTask(item); task.execute(notifyCompleted); } /** * Async task to enqueue a download request into DownloadManager. */ private class EnqueueDownloadRequestTask extends AsyncTask<Boolean, Void, Boolean> { private long mDownloadId; private final DownloadItem mDownloadItem; private int mFailureReason; private long mStartTime; public EnqueueDownloadRequestTask(DownloadItem downloadItem) { mDownloadItem = downloadItem; } @Override public Boolean doInBackground(Boolean... booleans) { boolean notifyCompleted = booleans[0]; Uri uri = Uri.parse(mDownloadItem.getDownloadInfo().getUrl()); DownloadManager.Request request; DownloadInfo info = mDownloadItem.getDownloadInfo(); try { request = new DownloadManager.Request(uri); } catch (IllegalArgumentException e) { Log.e(TAG, "Cannot download non http or https scheme"); // Use ERROR_UNHANDLED_HTTP_CODE so that it will be treated as // a server error. mFailureReason = DownloadManager.ERROR_UNHANDLED_HTTP_CODE; return false; } request.setMimeType(info.getMimeType()); try { if (notifyCompleted) { // Set downloaded file destination to /sdcard/Download or, should it be // set to one of several Environment.DIRECTORY* dirs depending on mimetype? request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, info.getFileName()); } else { File dir = new File(mContext.getExternalFilesDir(null), DOWNLOAD_DIRECTORY); if (dir.mkdir() || dir.isDirectory()) { File file = new File(dir, info.getFileName()); request.setDestinationUri(Uri.fromFile(file)); } else { Log.e(TAG, "Cannot create download directory"); mFailureReason = DownloadManager.ERROR_FILE_ERROR; return false; } } } catch (IllegalStateException e) { Log.e(TAG, "Cannot create download directory"); mFailureReason = DownloadManager.ERROR_FILE_ERROR; return false; } if (notifyCompleted) { // Let this downloaded file be scanned by MediaScanner - so that it can // show up in Gallery app, for example. request.allowScanningByMediaScanner(); request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); } else { request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE); } String description = info.getDescription(); if (TextUtils.isEmpty(description)) { description = info.getFileName(); } request.setDescription(description); request.setTitle(info.getFileName()); request.addRequestHeader("Cookie", info.getCookie()); request.addRequestHeader("Referer", info.getReferer()); request.addRequestHeader("User-Agent", info.getUserAgent()); DownloadManager manager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); try { mStartTime = System.currentTimeMillis(); mDownloadId = manager.enqueue(request); } catch (IllegalArgumentException e) { // See crbug.com/143499 for more details. Log.e(TAG, "Download failed: " + e); mFailureReason = DownloadManager.ERROR_UNKNOWN; return false; } catch (RuntimeException e) { // See crbug.com/490442 for more details. Log.e(TAG, "Failed to create target file on the external storage: " + e); mFailureReason = DownloadManager.ERROR_FILE_ERROR; return false; } return true; } @Override protected void onPostExecute(Boolean result) { boolean isPendingOMADownload = mOMADownloadHandler.isPendingOMADownload( mDownloadItem.getSystemDownloadId()); if (!result) { onDownloadFailed(mDownloadItem.getDownloadInfo().getFileName(), mFailureReason); recordDownloadCompletionStats(true, DOWNLOAD_STATUS_FAILED, 0, 0, 0); if (isPendingOMADownload) { mOMADownloadHandler.onDownloadFailed( mDownloadItem.getDownloadInfo(), mDownloadItem.getSystemDownloadId(), DownloadManager.ERROR_UNKNOWN, null); } return; } DownloadUtils.showDownloadStartToast(mContext); if (isPendingOMADownload) { // A new downloadId is generated, needs to update the OMADownloadHandler // about this. mOMADownloadHandler.updateDownloadInfo( mDownloadItem.getSystemDownloadId(), mDownloadId); // TODO(qinmin): use a file instead of shared prefs to save the // OMA information in case chrome is killed. This will allow us to // save more information like cookies and user agent. String notifyUri = mOMADownloadHandler.getInstallNotifyInfo(mDownloadId); if (!TextUtils.isEmpty(notifyUri)) { OMAEntry entry = new OMAEntry(mDownloadId, notifyUri); addOMADownloadToSharedPrefs(entry.generateSharedPrefsString()); } } if (mSystemDownloadIdMap.size() == 0) { mContext.registerReceiver(DownloadManagerService.this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } addUmaStatsEntry(new DownloadUmaStatsEntry( String.valueOf(mDownloadId), mStartTime, 0, false, true)); mDownloadItem.setSystemDownloadId(mDownloadId); mDownloadItem.setStartTime(mStartTime); mSystemDownloadIdMap.put(mDownloadId, mDownloadItem); } } /** * 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. * @return the intent to launch for the given download item. */ static Intent getLaunchIntentFromDownloadId( Context context, @Nullable String filePath, long downloadId, boolean isSupportedMimeType) { assert !ThreadUtils.runningOnUiThread(); DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); Uri contentUri = manager.getUriForDownloadedFile(downloadId); if (contentUri == null) return null; 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); } /** * 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); return (intent == null) ? false : ExternalNavigationDelegateImpl.resolveIntent( context, intent, true); } /** * Launch the intent for a given download item. * TODO(qinmin): Move this to DownloadManagerDelegate. * * @param downloadInfo Info about the downloaded item. * @param downloadId ID of the download item in DownloadManager. */ protected void openDownloadedContent(final DownloadInfo downloadInfo, final long downloadId) { final boolean isSupportedMimeType = isSupportedMimeType(downloadInfo.getMimeType()); new AsyncTask<Void, Void, Intent>() { @Override public Intent doInBackground(Void... params) { return getLaunchIntentFromDownloadId( mContext, downloadInfo.getFilePath(), downloadId, isSupportedMimeType); } @Override protected void onPostExecute(Intent intent) { if (intent == null) return; Context context = ContextUtils.getApplicationContext(); if (ExternalNavigationDelegateImpl.resolveIntent(context, intent, true)) { DownloadUtils.fireOpenIntentForDownload(context, intent); } } }.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 */ 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 */ protected 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); } } /** * Called to resume a paused download. * @param item Download item to resume. * @param hasUserGesture Whether the resumption is triggered by user gesture. */ @Override public void resumeDownload(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); } } nativeResumeDownload(getNativeDownloadManagerService(), item.getId(), item.getDownloadInfo().isOffTheRecord()); } /** * Called to cancel a download. * @param downloadGuid GUID of the download. * @param isOffTheRecord Whether the download is off the record. * @param isNotificationDismissed Whether cancel is caused by dismissing the notification. */ @Override public void cancelDownload( String downloadGuid, boolean isOffTheRecord, boolean isNotificationDismissed) { nativeCancelDownload(getNativeDownloadManagerService(), downloadGuid, isOffTheRecord, isNotificationDismissed); removeDownloadProgress(downloadGuid); recordDownloadFinishedUMA(DOWNLOAD_STATUS_CANCELLED, downloadGuid, 0); } /** * Called to pause a download. * @param downloadGuid GUID of the download. * @param isOffTheRecord Whether the download is off the record. */ @Override public void pauseDownload(String downloadGuid, boolean isOffTheRecord) { nativePauseDownload(getNativeDownloadManagerService(), downloadGuid, isOffTheRecord); DownloadProgress progress = mDownloadProgressMap.get(downloadGuid); // 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).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) { nativeRemoveDownload(getNativeDownloadManagerService(), downloadGuid, isOffTheRecord); removeDownloadProgress(downloadGuid); new AsyncTask<Void, Void, Void>() { @Override public Void doInBackground(Void... params) { mDownloadManagerDelegate.removeCompletedDownload(downloadGuid); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } /** * Checks whether the download can be opened by the browser. * @param downloadGuid GUID of the download. * @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( String downloadGuid, 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); } } /** * 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) { 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); } 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); } break; case DOWNLOAD_STATUS_CANCELLED: if (!useDownloadManager) { RecordHistogram.recordLongTimesHistogram( "MobileDownload.DownloadTime.ChromeNetworkStack.Cancel", totalDuration, TimeUnit.MILLISECONDS); RecordHistogram.recordCountHistogram( "MobileDownload.InterruptionsCount.ChromeNetworkStack.Cancel", numInterruptions); } break; default: break; } } @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); } break; case DOWNLOAD_STATUS_FAILED: onDownloadFailed( result.item.getDownloadInfo().getFileName(), result.failureReason); break; default: break; } } recordDownloadCompletionStats(true, result.downloadStatus, result.downloadTimeInMilliseconds, result.bytesDownloaded, 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, mContext, 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); Iterator<String> iterator = mAutoResumableDownloadIds.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)) { // Remove the pending resumable item so that the task won't be posted again on the // next connectivity change. iterator.remove(); // 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(new Runnable() { @Override public void run() { resumeDownload(progress.mDownloadItem, false); } }, mUpdateDelayInMillis); } } 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); 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) { mUmaEntries.add(DownloadUmaStatsEntry.parseFromString(entryString)); } } } /** Adds a new DownloadHistoryAdapter to the list. */ @Override public void addDownloadHistoryAdapter(DownloadHistoryAdapter adapter) { mHistoryAdapters.addObserver(adapter); } /** Removes a DownloadHistoryAdapter from the list. */ @Override public void removeDownloadHistoryAdapter(DownloadHistoryAdapter adapter) { mHistoryAdapters.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); } /** * 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, String guid, String displayName, String filepath, String url, String mimeType, long startTimestamp, long totalBytes, boolean hasBeenExternallyRemoved) { // Remap the MIME type first. File file = new File(filepath); String newMimeType = ChromeDownloadDelegate.remapGenericMimeType(mimeType, url, file.getName()); list.add(createDownloadItem( guid, displayName, filepath, url, newMimeType, startTimestamp, totalBytes, hasBeenExternallyRemoved)); } @CalledByNative private void onAllDownloadsRetrieved(final List<DownloadItem> list, boolean isOffTheRecord) { for (DownloadHistoryAdapter adapter : mHistoryAdapters) { adapter.onAllDownloadsRetrieved(list, isOffTheRecord); } } @CalledByNative private void onDownloadItemUpdated(int state, String guid, String displayName, String filepath, String url, String mimeType, long startTimestamp, long totalBytes, boolean isOffTheRecord, boolean hasBeenExternallyRemoved) { DownloadItem item = createDownloadItem( guid, displayName, filepath, url, mimeType, startTimestamp, totalBytes, hasBeenExternallyRemoved); for (DownloadHistoryAdapter adapter : mHistoryAdapters) { adapter.onDownloadItemUpdated(item, isOffTheRecord, state); } } @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( ContextUtils.getApplicationContext()); 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); } } @Override public void onMaxBandwidthChanged(double maxBandwidthMbps) {} @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 DownloadItem createDownloadItem(String guid, String displayName, String filepath, String url, String mimeType, long startTimestamp, long totalBytes, boolean hasBeenExternallyRemoved) { DownloadInfo.Builder builder = new DownloadInfo.Builder() .setDownloadGuid(guid) .setFileName(displayName) .setFilePath(filepath) .setUrl(url) .setMimeType(mimeType) .setContentLength(totalBytes); DownloadItem downloadItem = new DownloadItem(false, builder.build()); downloadItem.setStartTime(startTimestamp); downloadItem.setHasBeenExternallyRemoved(hasBeenExternallyRemoved); return downloadItem; } private static native boolean nativeIsSupportedMimeType(String mimeType); private native long nativeInit(); private native void nativeResumeDownload( long nativeDownloadManagerService, String downloadGuid, boolean isOffTheRecord); private native void nativeCancelDownload( long nativeDownloadManagerService, String downloadGuid, boolean isOffTheRecord, boolean isNotificationDismissed); 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); }