blob: 053d8348fa80f8ebcb7e1af29c7eecad136f9c83 [file] [log] [blame]
// 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 static org.chromium.chrome.browser.download.DownloadBroadcastManagerImpl.getServiceDelegate;
import static org.chromium.chrome.browser.download.DownloadSnackbarController.INVALID_NOTIFICATION_ID;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.shapes.OvalShape;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.StrictModeContext;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.profiles.OTRProfileID;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.notifications.NotificationManagerProxy;
import org.chromium.components.browser_ui.notifications.NotificationManagerProxyImpl;
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.Progress;
import org.chromium.components.offline_items_collection.PendingState;
import org.chromium.content_public.browser.BrowserStartupController;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Central director for updates related to downloads and notifications.
* - Receive updates about downloads through SystemDownloadNotifier (notifyDownloadPaused, etc).
* - Create notifications for downloads using DownloadNotificationFactory.
* - Update DownloadForegroundServiceManager about downloads, allowing it to start/stop service.
*/
public class DownloadNotificationService {
@IntDef({DownloadStatus.IN_PROGRESS, DownloadStatus.PAUSED, DownloadStatus.COMPLETED,
DownloadStatus.CANCELLED, DownloadStatus.FAILED})
@Retention(RetentionPolicy.SOURCE)
public @interface DownloadStatus {
int IN_PROGRESS = 0;
int PAUSED = 1;
int COMPLETED = 2;
int CANCELLED = 3;
int FAILED = 4;
}
public static final String ACTION_DOWNLOAD_CANCEL =
"org.chromium.chrome.browser.download.DOWNLOAD_CANCEL";
public static final String ACTION_DOWNLOAD_PAUSE =
"org.chromium.chrome.browser.download.DOWNLOAD_PAUSE";
public static final String ACTION_DOWNLOAD_RESUME =
"org.chromium.chrome.browser.download.DOWNLOAD_RESUME";
static final String ACTION_DOWNLOAD_OPEN = "org.chromium.chrome.browser.download.DOWNLOAD_OPEN";
static final String EXTRA_DOWNLOAD_CONTENTID_ID =
"org.chromium.chrome.browser.download.DownloadContentId_Id";
static final String EXTRA_DOWNLOAD_CONTENTID_NAMESPACE =
"org.chromium.chrome.browser.download.DownloadContentId_Namespace";
static final String EXTRA_DOWNLOAD_FILE_PATH = "DownloadFilePath";
static final String EXTRA_IS_SUPPORTED_MIME_TYPE = "IsSupportedMimeType";
static final String EXTRA_IS_OFF_THE_RECORD =
"org.chromium.chrome.browser.download.IS_OFF_THE_RECORD";
static final String EXTRA_OTR_PROFILE_ID =
"org.chromium.chrome.browser.download.OTR_PROFILE_ID";
// Used to propagate request state information for OfflineItems.StateAtCancel UMA.
static final String EXTRA_DOWNLOAD_STATE_AT_CANCEL =
"org.chromium.chrome.browser.download.OfflineItemsStateAtCancel";
static final String EXTRA_NOTIFICATION_BUNDLE_ICON_ID = "Chrome.NotificationBundleIconIdExtra";
static final String EXTRA_IS_AUTO_RESUMPTION =
"org.chromium.chrome.browser.download.IS_AUTO_RESUMPTION";
/** Notification Id starting value, to avoid conflicts from IDs used in prior versions. */
private static final int STARTING_NOTIFICATION_ID = 1000000;
private static final int MAX_RESUMPTION_ATTEMPT_LEFT = 5;
@VisibleForTesting
final List<ContentId> mDownloadsInProgress = new ArrayList<ContentId>();
private NotificationManagerProxy mNotificationManager;
private Bitmap mDownloadSuccessLargeIcon;
private DownloadSharedPreferenceHelper mDownloadSharedPreferenceHelper;
private DownloadForegroundServiceManager mDownloadForegroundServiceManager;
private static class LazyHolder {
private static final DownloadNotificationService INSTANCE =
new DownloadNotificationService();
}
/**
* Creates DownloadNotificationService.
*/
public static DownloadNotificationService getInstance() {
return LazyHolder.INSTANCE;
}
@VisibleForTesting
DownloadNotificationService() {
mNotificationManager =
new NotificationManagerProxyImpl(ContextUtils.getApplicationContext());
mDownloadSharedPreferenceHelper = DownloadSharedPreferenceHelper.getInstance();
mDownloadForegroundServiceManager = new DownloadForegroundServiceManager();
}
@VisibleForTesting
void setDownloadForegroundServiceManager(
DownloadForegroundServiceManager downloadForegroundServiceManager) {
mDownloadForegroundServiceManager = downloadForegroundServiceManager;
}
/**
* @return Whether or not there are any current resumable downloads being tracked. These
* tracked downloads may not currently be showing notifications.
*/
static boolean isTrackingResumableDownloads(Context context) {
List<DownloadSharedPreferenceEntry> entries =
DownloadSharedPreferenceHelper.getInstance().getEntries();
for (DownloadSharedPreferenceEntry entry : entries) {
if (canResumeDownload(context, entry)) return true;
}
return false;
}
/**
* Track in-progress downloads here.
* @param id The {@link ContentId} of the download that has been started and should be tracked.
*/
private void startTrackingInProgressDownload(ContentId id) {
if (!mDownloadsInProgress.contains(id)) mDownloadsInProgress.add(id);
}
/**
* Stop tracking the download represented by {@code id}.
* @param id The {@link ContentId} of the download that has been paused or
* canceled and shouldn't be tracked.
*/
private void stopTrackingInProgressDownload(ContentId id) {
mDownloadsInProgress.remove(id);
}
/**
* Adds or updates an in-progress download notification.
* @param id The {@link ContentId} of the download.
* @param fileName File name of the download.
* @param progress The current download progress.
* @param bytesReceived Total number of bytes received.
* @param timeRemainingInMillis Remaining download time in milliseconds.
* @param startTime Time when download started.
* @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular
* mode.
* @param canDownloadWhileMetered Whether the download can happen in metered network.
* @param isTransient Whether or not clicking on the download should launch
* downloads home.
* @param icon A {@link Bitmap} to be used as the large icon for display.
* @param originalUrl The original url of the downloaded file.
* @param shouldPromoteOrigin Whether the origin should be displayed in the notification.
*/
@VisibleForTesting
public void notifyDownloadProgress(ContentId id, String fileName, Progress progress,
long bytesReceived, long timeRemainingInMillis, long startTime,
OTRProfileID otrProfileID, boolean canDownloadWhileMetered, boolean isTransient,
Bitmap icon, String originalUrl, boolean shouldPromoteOrigin) {
updateActiveDownloadNotification(id, fileName, progress, timeRemainingInMillis, startTime,
otrProfileID, canDownloadWhileMetered, isTransient, icon, originalUrl,
shouldPromoteOrigin, false, PendingState.NOT_PENDING);
}
/**
* Adds or updates a pending download notification.
* @param id The {@link ContentId} of the download.
* @param fileName File name of the download.
* @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular
* mode.
* @param canDownloadWhileMetered Whether the download can happen in metered network.
* @param isTransient Whether or not clicking on the download should launch
* downloads home.
* @param icon A {@link Bitmap} to be used as the large icon for display.
* @param originalUrl The original url of the downloaded file.
* @param shouldPromoteOrigin Whether the origin should be displayed in the notification.
* @param pendingState Reason download is pending.
*/
void notifyDownloadPending(ContentId id, String fileName, OTRProfileID otrProfileID,
boolean canDownloadWhileMetered, boolean isTransient, Bitmap icon, String originalUrl,
boolean shouldPromoteOrigin, boolean hasUserGesture, @PendingState int pendingState) {
updateActiveDownloadNotification(id, fileName, Progress.createIndeterminateProgress(), 0, 0,
otrProfileID, canDownloadWhileMetered, isTransient, icon, originalUrl,
shouldPromoteOrigin, hasUserGesture, pendingState);
}
/**
* Helper method to update the notification for an active download, the download is either in
* progress or pending.
* @param id The {@link ContentId} of the download.
* @param fileName File name of the download.
* @param progress The current download progress.
* @param timeRemainingInMillis Remaining download time in milliseconds or -1 if it is
* unknown.
* @param startTime Time when download started.
* @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular
* mode.
* @param canDownloadWhileMetered Whether the download can happen in metered network.
* @param isTransient Whether or not clicking on the download should launch
* downloads home.
* @param icon A {@link Bitmap} to be used as the large icon for display.
* @param originalUrl The original url of the downloaded file.
* @param shouldPromoteOrigin Whether the origin should be displayed in the notification.
* @param pendingState Reason download is pending.
*/
private void updateActiveDownloadNotification(ContentId id, String fileName, Progress progress,
long timeRemainingInMillis, long startTime, OTRProfileID otrProfileID,
boolean canDownloadWhileMetered, boolean isTransient, Bitmap icon, String originalUrl,
boolean shouldPromoteOrigin, boolean hasUserGesture, @PendingState int pendingState) {
int notificationId = getNotificationId(id);
Context context = ContextUtils.getApplicationContext();
DownloadUpdate downloadUpdate = new DownloadUpdate.Builder()
.setContentId(id)
.setFileName(fileName)
.setProgress(progress)
.setTimeRemainingInMillis(timeRemainingInMillis)
.setStartTime(startTime)
.setOTRProfileID(otrProfileID)
.setIsTransient(isTransient)
.setIcon(icon)
.setOriginalUrl(originalUrl)
.setShouldPromoteOrigin(shouldPromoteOrigin)
.setNotificationId(notificationId)
.setPendingState(pendingState)
.build();
Notification notification = DownloadNotificationFactory.buildNotification(
context, DownloadStatus.IN_PROGRESS, downloadUpdate, notificationId);
updateNotification(notificationId, notification, id,
new DownloadSharedPreferenceEntry(id, notificationId, otrProfileID,
canDownloadWhileMetered, fileName, true, isTransient));
// If the notification is allowed to start foreground service, or if the app is already
// foreground, ask the foreground service manager to handle the notification.
if (canStartForegroundService() || mDownloadForegroundServiceManager.isServiceBound()) {
mDownloadForegroundServiceManager.updateDownloadStatus(
context, DownloadStatus.IN_PROGRESS, notificationId, notification);
}
startTrackingInProgressDownload(id);
}
private void cancelNotification(int notificationId) {
// TODO(b/65052774): Add back NOTIFICATION_NAMESPACE when able to.
mNotificationManager.cancel(notificationId);
}
/**
* Removes a download notification and all associated tracking. This method relies on the
* caller to provide the notification id, which is useful in the case where the internal
* tracking doesn't exist (e.g. in the case of a successful download, where we show the download
* completed notification and remove our internal state tracking).
* @param notificationId Notification ID of the download
* @param id The {@link ContentId} of the download.
*/
public void cancelNotification(int notificationId, ContentId id) {
cancelNotification(notificationId);
mDownloadSharedPreferenceHelper.removeSharedPreferenceEntry(id);
stopTrackingInProgressDownload(id);
}
/**
* Called when a download is canceled given the notification ID.
* @param id The {@link ContentId} of the download.
* @param notificationId Notification ID of the download.
* @param hasUserGesture Whether cancel is triggered by user gesture.
*/
@VisibleForTesting
public void notifyDownloadCanceled(ContentId id, int notificationId, boolean hasUserGesture) {
mDownloadForegroundServiceManager.updateDownloadStatus(ContextUtils.getApplicationContext(),
DownloadStatus.CANCELLED, notificationId, null);
cancelNotification(notificationId, id);
}
/**
* Called when a download is canceled. This method uses internal tracking to try to find the
* notification id to cancel.
* Called when a download is canceled.
* @param id The {@link ContentId} of the download.
* @param hasUserGesture Whether cancel is triggered by user gesture.
*/
@VisibleForTesting
public void notifyDownloadCanceled(ContentId id, boolean hasUserGesture) {
DownloadSharedPreferenceEntry entry =
mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(id);
if (entry == null) return;
notifyDownloadCanceled(id, entry.notificationId, hasUserGesture);
}
/**
* Change a download notification to paused state.
* @param id The {@link ContentId} of the download.
* @param fileName File name of the download.
* @param isResumable Whether download can be resumed.
* @param isAutoResumable Whether download is can be resumed automatically.
* @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular mode.
* @param isTransient Whether or not clicking on the download should launch downloads
* home.
* @param icon A {@link Bitmap} to be used as the large icon for display.
* @param originalUrl The original url of the downloaded file.
* @param shouldPromoteOrigin Whether the origin should be displayed in the notification.
* @param forceRebuild Whether the notification was forcibly relaunched.
* @param pendingState Reason download is pending.
*/
@VisibleForTesting
void notifyDownloadPaused(ContentId id, String fileName, boolean isResumable,
boolean isAutoResumable, OTRProfileID otrProfileID, boolean isTransient, Bitmap icon,
String originalUrl, boolean shouldPromoteOrigin, boolean hasUserGesture,
boolean forceRebuild, @PendingState int pendingState) {
DownloadSharedPreferenceEntry entry =
mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(id);
if (!isResumable) {
// TODO(cmsy): Use correct FailState.
notifyDownloadFailed(id, fileName, icon, originalUrl, shouldPromoteOrigin, otrProfileID,
FailState.CANNOT_DOWNLOAD);
return;
}
// If download is already paused, do nothing.
if (entry != null && !entry.isAutoResumable && !forceRebuild) return;
boolean canDownloadWhileMetered = entry == null ? false : entry.canDownloadWhileMetered;
// If download is interrupted due to network disconnection, show download pending state.
if (isAutoResumable || pendingState != PendingState.NOT_PENDING) {
notifyDownloadPending(id, fileName, otrProfileID, canDownloadWhileMetered, isTransient,
icon, originalUrl, shouldPromoteOrigin, hasUserGesture, pendingState);
stopTrackingInProgressDownload(id);
return;
}
int notificationId = entry == null ? getNotificationId(id) : entry.notificationId;
Context context = ContextUtils.getApplicationContext();
DownloadUpdate downloadUpdate = new DownloadUpdate.Builder()
.setContentId(id)
.setFileName(fileName)
.setOTRProfileID(otrProfileID)
.setIsTransient(isTransient)
.setIcon(icon)
.setOriginalUrl(originalUrl)
.setShouldPromoteOrigin(shouldPromoteOrigin)
.setNotificationId(notificationId)
.build();
Notification notification = DownloadNotificationFactory.buildNotification(
context, DownloadStatus.PAUSED, downloadUpdate, notificationId);
updateNotification(notificationId, notification, id,
new DownloadSharedPreferenceEntry(id, notificationId, otrProfileID,
canDownloadWhileMetered, fileName, isAutoResumable, isTransient));
mDownloadForegroundServiceManager.updateDownloadStatus(
context, DownloadStatus.PAUSED, notificationId, notification);
stopTrackingInProgressDownload(id);
}
/**
* Add a download successful notification.
* @param id The {@link ContentId} of the download.
* @param filePath Full path to the download.
* @param fileName Filename of the download.
* @param systemDownloadId Download ID assigned by system DownloadManager.
* @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular mode.
* @param isSupportedMimeType Whether the MIME type can be viewed inside browser.
* @param isOpenable Whether or not this download can be opened.
* @param icon A {@link Bitmap} to be used as the large icon for display.
* @param originalUrl The original url of the downloaded file.
* @param shouldPromoteOrigin Whether the origin should be displayed in the notification.
* @param referrer Referrer of the downloaded file.
* @param totalBytes The total number of bytes downloaded (size of file).
* @return ID of the successful download notification. Used for removing the
* notification when user click on the snackbar.
*/
@VisibleForTesting
public int notifyDownloadSuccessful(ContentId id, String filePath, String fileName,
long systemDownloadId, OTRProfileID otrProfileID, boolean isSupportedMimeType,
boolean isOpenable, Bitmap icon, String originalUrl, boolean shouldPromoteOrigin,
String referrer, long totalBytes) {
Context context = ContextUtils.getApplicationContext();
int notificationId = getNotificationId(id);
boolean needsDefaultIcon = icon == null || OTRProfileID.isOffTheRecord(otrProfileID);
if (mDownloadSuccessLargeIcon == null && needsDefaultIcon) {
Bitmap bitmap =
BitmapFactory.decodeResource(context.getResources(), R.drawable.offline_pin);
mDownloadSuccessLargeIcon = getLargeNotificationIcon(bitmap);
}
if (needsDefaultIcon) icon = mDownloadSuccessLargeIcon;
DownloadUpdate downloadUpdate = new DownloadUpdate.Builder()
.setContentId(id)
.setFileName(fileName)
.setFilePath(filePath)
.setSystemDownload(systemDownloadId)
.setOTRProfileID(otrProfileID)
.setIsSupportedMimeType(isSupportedMimeType)
.setIsOpenable(isOpenable)
.setIcon(icon)
.setNotificationId(notificationId)
.setOriginalUrl(originalUrl)
.setShouldPromoteOrigin(shouldPromoteOrigin)
.setReferrer(referrer)
.setTotalBytes(totalBytes)
.build();
Notification notification = DownloadNotificationFactory.buildNotification(
context, DownloadStatus.COMPLETED, downloadUpdate, notificationId);
updateNotification(notificationId, notification, id, null);
mDownloadForegroundServiceManager.updateDownloadStatus(
context, DownloadStatus.COMPLETED, notificationId, notification);
stopTrackingInProgressDownload(id);
return notificationId;
}
/**
* Add a download failed notification.
* @param id The {@link ContentId} of the download.
* @param fileName Filename of the download.
* @param icon A {@link Bitmap} to be used as the large icon for display.
* @param originalUrl The original url of the downloaded file.
* @param shouldPromoteOrigin Whether the origin should be displayed in the notification.
* @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular mode.
* @param failState Reason why download failed.
*/
@VisibleForTesting
public void notifyDownloadFailed(ContentId id, String fileName, Bitmap icon, String originalUrl,
boolean shouldPromoteOrigin, OTRProfileID otrProfileID, @FailState int failState) {
// If the download is not in history db, fileName could be empty. Get it from
// SharedPreferences.
if (TextUtils.isEmpty(fileName)) {
DownloadSharedPreferenceEntry entry =
mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(id);
if (entry == null) return;
fileName = entry.fileName;
}
int notificationId = getNotificationId(id);
Context context = ContextUtils.getApplicationContext();
DownloadUpdate downloadUpdate = new DownloadUpdate.Builder()
.setContentId(id)
.setFileName(fileName)
.setIcon(icon)
.setOTRProfileID(otrProfileID)
.setOriginalUrl(originalUrl)
.setShouldPromoteOrigin(shouldPromoteOrigin)
.setFailState(failState)
.build();
Notification notification = DownloadNotificationFactory.buildNotification(
context, DownloadStatus.FAILED, downloadUpdate, notificationId);
updateNotification(notificationId, notification, id, null);
mDownloadForegroundServiceManager.updateDownloadStatus(
context, DownloadStatus.FAILED, notificationId, notification);
stopTrackingInProgressDownload(id);
}
private Bitmap getLargeNotificationIcon(Bitmap bitmap) {
Resources resources = ContextUtils.getApplicationContext().getResources();
int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height);
int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width);
final OvalShape circle = new OvalShape();
circle.resize(width, height);
final Paint paint = new Paint();
paint.setColor(ApiCompatibilityUtils.getColor(resources, R.color.google_blue_grey_500));
final Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
circle.draw(canvas, paint);
float leftOffset = (width - bitmap.getWidth()) / 2f;
float topOffset = (height - bitmap.getHeight()) / 2f;
if (leftOffset >= 0 && topOffset >= 0) {
canvas.drawBitmap(bitmap, leftOffset, topOffset, null);
} else {
// Scale down the icon into the notification icon dimensions
canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()),
new Rect(0, 0, width, height), null);
}
return result;
}
@VisibleForTesting
void updateNotification(int id, Notification notification) {
// TODO(b/65052774): Add back NOTIFICATION_NAMESPACE when able to.
// Disabling StrictMode to avoid violations (crbug.com/789134).
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
mNotificationManager.notify(id, notification);
}
}
private void updateNotification(int notificationId, Notification notification, ContentId id,
DownloadSharedPreferenceEntry entry) {
updateNotification(notificationId, notification);
trackNotificationUma(id, notification);
if (entry != null) {
mDownloadSharedPreferenceHelper.addOrReplaceSharedPreferenceEntry(entry);
} else {
mDownloadSharedPreferenceHelper.removeSharedPreferenceEntry(id);
}
}
private void trackNotificationUma(ContentId id, Notification notification) {
// Check if we already have an entry in the DownloadSharedPreferenceHelper. This is a
// reasonable indicator for whether or not a notification is already showing (or at least if
// we had built one for this download before.
if (mDownloadSharedPreferenceHelper.hasEntry(id)) return;
NotificationUmaTracker.getInstance().onNotificationShown(
LegacyHelpers.isLegacyOfflinePage(id)
? NotificationUmaTracker.SystemNotificationType.DOWNLOAD_PAGES
: NotificationUmaTracker.SystemNotificationType.DOWNLOAD_FILES,
notification);
}
private static boolean canResumeDownload(Context context, DownloadSharedPreferenceEntry entry) {
if (entry == null) return false;
if (!entry.isAutoResumable) return false;
boolean isNetworkMetered = DownloadManagerService.isActiveNetworkMetered(context);
return entry.canDownloadWhileMetered || !isNetworkMetered;
}
/**
* Resumes all pending downloads from SharedPreferences. If a download is
* already in progress, do nothing.
*/
void resumeAllPendingDownloads() {
if (CachedFeatureFlags.isEnabled(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE)) {
return;
}
// Limit the number of auto resumption attempts in case Chrome falls into a vicious cycle.
DownloadResumptionScheduler.getDownloadResumptionScheduler().cancel();
int numAutoResumptionAtemptLeft = getResumptionAttemptLeft();
if (numAutoResumptionAtemptLeft <= 0) return;
numAutoResumptionAtemptLeft--;
updateResumptionAttemptLeft(numAutoResumptionAtemptLeft);
// Go through and check which downloads to resume.
List<DownloadSharedPreferenceEntry> entries = mDownloadSharedPreferenceHelper.getEntries();
for (int i = 0; i < entries.size(); ++i) {
DownloadSharedPreferenceEntry entry = entries.get(i);
if (!canResumeDownload(ContextUtils.getApplicationContext(), entry)) continue;
if (mDownloadsInProgress.contains(entry.id)) continue;
notifyDownloadPending(entry.id, entry.fileName, entry.otrProfileID,
entry.canDownloadWhileMetered, entry.isTransient, null, null, false, false,
PendingState.PENDING_NETWORK);
Intent intent = new Intent();
intent.setAction(ACTION_DOWNLOAD_RESUME);
intent.putExtra(EXTRA_DOWNLOAD_CONTENTID_ID, entry.id.id);
intent.putExtra(EXTRA_DOWNLOAD_CONTENTID_NAMESPACE, entry.id.namespace);
intent.putExtra(EXTRA_IS_AUTO_RESUMPTION, true);
resumeDownload(intent);
}
}
@VisibleForTesting
void resumeDownload(Intent intent) {
DownloadBroadcastManagerImpl.startDownloadBroadcastManager(
ContextUtils.getApplicationContext(), intent);
}
/**
* Return the notification ID for the given download {@link ContentId}.
* @param id the {@link ContentId} of the download.
* @return notification ID to be used.
*/
private int getNotificationId(ContentId id) {
DownloadSharedPreferenceEntry entry =
mDownloadSharedPreferenceHelper.getDownloadSharedPreferenceEntry(id);
if (entry != null) return entry.notificationId;
return getNextNotificationId();
}
/**
* Get the next notificationId based on stored value and update shared preferences.
* @return notificationId that is next based on stored value.
*/
private static int getNextNotificationId() {
int nextNotificationId = SharedPreferencesManager.getInstance().readInt(
ChromePreferenceKeys.DOWNLOAD_NEXT_DOWNLOAD_NOTIFICATION_ID,
STARTING_NOTIFICATION_ID);
int nextNextNotificationId = nextNotificationId == Integer.MAX_VALUE
? STARTING_NOTIFICATION_ID
: nextNotificationId + 1;
SharedPreferencesManager.getInstance().writeInt(
ChromePreferenceKeys.DOWNLOAD_NEXT_DOWNLOAD_NOTIFICATION_ID,
nextNextNotificationId);
return nextNotificationId;
}
static int getNewNotificationIdFor(int oldNotificationId) {
int newNotificationId = getNextNotificationId();
DownloadSharedPreferenceHelper downloadSharedPreferenceHelper =
DownloadSharedPreferenceHelper.getInstance();
List<DownloadSharedPreferenceEntry> entries = downloadSharedPreferenceHelper.getEntries();
for (DownloadSharedPreferenceEntry entry : entries) {
if (entry.notificationId == oldNotificationId) {
DownloadSharedPreferenceEntry newEntry = new DownloadSharedPreferenceEntry(entry.id,
newNotificationId, entry.otrProfileID, entry.canDownloadWhileMetered,
entry.fileName, entry.isAutoResumable, entry.isTransient);
downloadSharedPreferenceHelper.addOrReplaceSharedPreferenceEntry(
newEntry, true /* forceCommit */);
break;
}
}
return newNotificationId;
}
/**
* Helper method to update the remaining number of background resumption attempts left.
*
* @param numAutoResumptionAttemptLeft the number of auto resumption attempts left.
*/
private static void updateResumptionAttemptLeft(int numAutoResumptionAttemptLeft) {
SharedPreferencesManager.getInstance().writeInt(
ChromePreferenceKeys.DOWNLOAD_AUTO_RESUMPTION_ATTEMPT_LEFT,
numAutoResumptionAttemptLeft);
}
/** Helper method to get the remaining number of background resumption attempts left. */
private static int getResumptionAttemptLeft() {
return SharedPreferencesManager.getInstance().readInt(
ChromePreferenceKeys.DOWNLOAD_AUTO_RESUMPTION_ATTEMPT_LEFT,
MAX_RESUMPTION_ATTEMPT_LEFT);
}
/** Helper method to clear the remaining number of background resumption attempts left. */
static void clearResumptionAttemptLeft() {
SharedPreferencesManager.getInstance().removeKey(
ChromePreferenceKeys.DOWNLOAD_AUTO_RESUMPTION_ATTEMPT_LEFT);
}
void onForegroundServiceRestarted(int pinnedNotificationId) {
// In API < 24, notifications pinned to the foreground will get killed with the service.
// Fix this by relaunching the notification that was pinned to the service as the service
// dies, if there is one.
relaunchPinnedNotification(pinnedNotificationId);
updateNotificationsForShutdown();
resumeAllPendingDownloads();
}
void onForegroundServiceTaskRemoved() {
// If we've lost all Activities, cancel the off the record downloads.
if (ApplicationStatus.isEveryActivityDestroyed()) {
cancelOffTheRecordDownloads();
}
}
void onForegroundServiceDestroyed() {
updateNotificationsForShutdown();
rescheduleDownloads();
}
/**
* Given the id of the notification that was pinned to the service when it died, give the
* notification a new id in order to rebuild and relaunch the notification.
* @param pinnedNotificationId Id of the notification pinned to the service when it died.
*/
private void relaunchPinnedNotification(int pinnedNotificationId) {
// If there was no notification pinned to the service, no correction is necessary.
if (pinnedNotificationId == INVALID_NOTIFICATION_ID) return;
List<DownloadSharedPreferenceEntry> entries = mDownloadSharedPreferenceHelper.getEntries();
List<DownloadSharedPreferenceEntry> copies =
new ArrayList<DownloadSharedPreferenceEntry>(entries);
for (DownloadSharedPreferenceEntry entry : copies) {
if (entry.notificationId == pinnedNotificationId) {
// Get new notification id that is not associated with the service.
DownloadSharedPreferenceEntry updatedEntry =
new DownloadSharedPreferenceEntry(entry.id, getNextNotificationId(),
entry.otrProfileID, entry.canDownloadWhileMetered, entry.fileName,
entry.isAutoResumable, entry.isTransient);
mDownloadSharedPreferenceHelper.addOrReplaceSharedPreferenceEntry(updatedEntry);
// Right now this only happens in the paused case, so re-build and re-launch the
// paused notification, with the updated notification id..
notifyDownloadPaused(updatedEntry.id, updatedEntry.fileName, true /* isResumable */,
updatedEntry.isAutoResumable, updatedEntry.otrProfileID,
updatedEntry.isTransient, null /* icon */, null /* originalUrl */,
false /* shouldPromoteOrigin */, true /* hasUserGesture */,
true /* forceRebuild */, PendingState.NOT_PENDING);
return;
}
}
}
private void updateNotificationsForShutdown() {
cancelOffTheRecordDownloads();
List<DownloadSharedPreferenceEntry> entries = mDownloadSharedPreferenceHelper.getEntries();
for (DownloadSharedPreferenceEntry entry : entries) {
if (OTRProfileID.isOffTheRecord(entry.otrProfileID)) continue;
// Move all regular downloads to pending. Don't propagate the pause because
// if native is still working and it triggers an update, then the service will be
// restarted.
notifyDownloadPaused(entry.id, entry.fileName, true, true, null, entry.isTransient,
null, null, false, false, false, PendingState.PENDING_NETWORK);
}
}
public void cancelOffTheRecordDownloads() {
boolean cancelActualDownload = BrowserStartupController.getInstance().isFullBrowserStarted()
&& Profile.getLastUsedRegularProfile().hasPrimaryOTRProfile();
List<DownloadSharedPreferenceEntry> entries = mDownloadSharedPreferenceHelper.getEntries();
List<DownloadSharedPreferenceEntry> copies =
new ArrayList<DownloadSharedPreferenceEntry>(entries);
for (DownloadSharedPreferenceEntry entry : copies) {
if (!OTRProfileID.isOffTheRecord(entry.otrProfileID)) continue;
ContentId id = entry.id;
notifyDownloadCanceled(id, false);
if (cancelActualDownload) {
DownloadServiceDelegate delegate = getServiceDelegate(id);
DownloadMetrics.recordDownloadCancel(DownloadMetrics.CancelFrom.CANCEL_SHUTDOWN);
delegate.cancelDownload(id, entry.otrProfileID);
delegate.destroyServiceDelegate();
}
}
}
private void rescheduleDownloads() {
if (getResumptionAttemptLeft() <= 0) return;
DownloadResumptionScheduler.getDownloadResumptionScheduler().scheduleIfNecessary();
}
private boolean canStartForegroundService() {
if (AppHooks.get().canStartForegroundServiceWhileInvisible()) return true;
return ApplicationStatus.hasVisibleActivities();
}
}