blob: e8ee6619b5e2dfc52e50a052512ca372f856ed52 [file] [log] [blame]
// Copyright 2017 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 android.app.DownloadManager.ACTION_NOTIFICATION_CLICKED;
import static android.app.DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_CANCEL;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_OPEN;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_PAUSE;
import static org.chromium.chrome.browser.download.DownloadNotificationService.ACTION_DOWNLOAD_RESUME;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_CONTENTID_ID;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_CONTENTID_NAMESPACE;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_FILE_PATH;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_DOWNLOAD_STATE_AT_CANCEL;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_IS_OFF_THE_RECORD;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_IS_SUPPORTED_MIME_TYPE;
import static org.chromium.chrome.browser.download.DownloadNotificationService.EXTRA_NOTIFICATION_BUNDLE_ICON_ID;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import com.google.ipc.invalidation.util.Preconditions;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.media.MediaViewerUtils;
import org.chromium.chrome.browser.notifications.ChromeNotificationBuilder;
import org.chromium.chrome.browser.notifications.NotificationBuilderFactory;
import org.chromium.chrome.browser.notifications.NotificationConstants;
import org.chromium.chrome.browser.notifications.NotificationMetadata;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.notifications.PendingIntentProvider;
import org.chromium.chrome.browser.notifications.channels.ChannelDefinitions;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.LegacyHelpers;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.PendingState;
import org.chromium.components.url_formatter.UrlFormatter;
/**
* Creates and updates notifications related to downloads.
*/
public final class DownloadNotificationFactory {
// Limit file name to 25 characters. TODO(qinmin): use different limit for different devices?
public static final int MAX_FILE_NAME_LENGTH = 25;
// Limit the origin length so that the eTLD+1 cannot be hidden. If the origin exceeds this
// length the eTLD+1 is extracted and shown.
public static final int MAX_ORIGIN_LENGTH = 40;
/**
* Builds a downloads notification based on the status of the download and its information. All
* changes to this function should consider the difference between normal profile and off the
* record profile.
* @param context of the download.
* @param downloadStatus (in progress, paused, successful, failed, deleted, or summary).
* @param downloadUpdate information about the download (ie. contentId, fileName, icon,
* isOffTheRecord, etc).
* @param notificationId The notification id passed to {@link
* android.app.NotificationManager#notify(String, int, Notification)}.
* @return Notification that is built based on these parameters.
*/
public static Notification buildNotification(Context context,
@DownloadNotificationService.DownloadStatus int downloadStatus,
DownloadUpdate downloadUpdate, int notificationId) {
ChromeNotificationBuilder builder =
NotificationBuilderFactory
.createChromeNotificationBuilder(true /* preferCompat */,
ChannelDefinitions.ChannelId.DOWNLOADS,
null /* remoteAppPackageName */,
new NotificationMetadata(LegacyHelpers.isLegacyDownload(
downloadUpdate.getContentId())
? NotificationUmaTracker.SystemNotificationType
.DOWNLOAD_FILES
: NotificationUmaTracker.SystemNotificationType
.DOWNLOAD_PAGES,
null /* tag */, notificationId))
.setLocalOnly(true)
.setGroup(NotificationConstants.GROUP_DOWNLOADS)
.setAutoCancel(true);
String contentText;
int iconId;
@NotificationUmaTracker.ActionType
int cancelActionType,
pauseActionType, resumeActionType;
if (LegacyHelpers.isLegacyDownload(downloadUpdate.getContentId())) {
cancelActionType = NotificationUmaTracker.ActionType.DOWNLOAD_CANCEL;
pauseActionType = NotificationUmaTracker.ActionType.DOWNLOAD_PAUSE;
resumeActionType = NotificationUmaTracker.ActionType.DOWNLOAD_RESUME;
} else {
cancelActionType = NotificationUmaTracker.ActionType.DOWNLOAD_PAGE_CANCEL;
pauseActionType = NotificationUmaTracker.ActionType.DOWNLOAD_PAGE_PAUSE;
resumeActionType = NotificationUmaTracker.ActionType.DOWNLOAD_PAGE_RESUME;
}
switch (downloadStatus) {
case DownloadNotificationService.DownloadStatus.IN_PROGRESS:
Preconditions.checkNotNull(downloadUpdate.getProgress());
Preconditions.checkNotNull(downloadUpdate.getContentId());
Preconditions.checkArgument(downloadUpdate.getNotificationId() != -1);
if (downloadUpdate.getIsDownloadPending()) {
contentText =
DownloadUtils.getPendingStatusString(downloadUpdate.getPendingState());
} else {
// Incognito mode should hide download progress details like file size.
OfflineItem.Progress progress = downloadUpdate.getIsOffTheRecord()
? OfflineItem.Progress.createIndeterminateProgress()
: downloadUpdate.getProgress();
contentText = DownloadUtils.getProgressTextForNotification(progress);
}
iconId = downloadUpdate.getIsDownloadPending()
? R.drawable.ic_download_pending
: android.R.drawable.stat_sys_download;
Intent pauseIntent = buildActionIntent(context, ACTION_DOWNLOAD_PAUSE,
downloadUpdate.getContentId(), downloadUpdate.getIsOffTheRecord());
Intent cancelIntent = buildActionIntent(context, ACTION_DOWNLOAD_CANCEL,
downloadUpdate.getContentId(), downloadUpdate.getIsOffTheRecord());
switch (downloadUpdate.getPendingState()) {
case PendingState.NOT_PENDING:
cancelIntent.putExtra(EXTRA_DOWNLOAD_STATE_AT_CANCEL,
DownloadNotificationUmaHelper.StateAtCancel.DOWNLOADING);
break;
case PendingState.PENDING_NETWORK:
cancelIntent.putExtra(EXTRA_DOWNLOAD_STATE_AT_CANCEL,
DownloadNotificationUmaHelper.StateAtCancel.PENDING_NETWORK);
break;
case PendingState.PENDING_ANOTHER_DOWNLOAD:
cancelIntent.putExtra(EXTRA_DOWNLOAD_STATE_AT_CANCEL,
DownloadNotificationUmaHelper.StateAtCancel
.PENDING_ANOTHER_DOWNLOAD);
break;
}
builder.setOngoing(true)
.setPriorityBeforeO(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(false)
.addAction(R.drawable.ic_pause_white_24dp,
context.getResources().getString(
R.string.download_notification_pause_button),
buildPendingIntentProvider(
context, pauseIntent, downloadUpdate.getNotificationId()),
pauseActionType)
.addAction(R.drawable.btn_close_white,
context.getResources().getString(
R.string.download_notification_cancel_button),
buildPendingIntentProvider(
context, cancelIntent, downloadUpdate.getNotificationId()),
cancelActionType);
if (!downloadUpdate.getIsOffTheRecord())
builder.setLargeIcon(downloadUpdate.getIcon());
if (!downloadUpdate.getIsDownloadPending()) {
boolean indeterminate = downloadUpdate.getProgress().isIndeterminate();
builder.setProgress(100,
indeterminate ? -1 : downloadUpdate.getProgress().getPercentage(),
indeterminate);
}
if (!downloadUpdate.getProgress().isIndeterminate()
&& !downloadUpdate.getIsOffTheRecord()
&& downloadUpdate.getTimeRemainingInMillis() >= 0
&& !LegacyHelpers.isLegacyOfflinePage(downloadUpdate.getContentId())) {
String subText = DownloadUtils.formatRemainingTime(
context, downloadUpdate.getTimeRemainingInMillis());
setSubText(builder, subText);
}
if (downloadUpdate.getStartTime() > 0) {
builder.setWhen(downloadUpdate.getStartTime());
}
break;
case DownloadNotificationService.DownloadStatus.PAUSED:
Preconditions.checkNotNull(downloadUpdate.getContentId());
Preconditions.checkArgument(downloadUpdate.getNotificationId() != -1);
contentText =
context.getResources().getString(R.string.download_notification_paused);
iconId = R.drawable.ic_download_pause;
Intent resumeIntent = buildActionIntent(context, ACTION_DOWNLOAD_RESUME,
downloadUpdate.getContentId(), downloadUpdate.getIsOffTheRecord());
cancelIntent = buildActionIntent(context, ACTION_DOWNLOAD_CANCEL,
downloadUpdate.getContentId(), downloadUpdate.getIsOffTheRecord());
cancelIntent.putExtra(EXTRA_DOWNLOAD_STATE_AT_CANCEL,
DownloadNotificationUmaHelper.StateAtCancel.PAUSED);
builder.setAutoCancel(false)
.addAction(R.drawable.ic_file_download_white_24dp,
context.getResources().getString(
R.string.download_notification_resume_button),
buildPendingIntentProvider(
context, resumeIntent, downloadUpdate.getNotificationId()),
resumeActionType)
.addAction(R.drawable.btn_close_white,
context.getResources().getString(
R.string.download_notification_cancel_button),
buildPendingIntentProvider(
context, cancelIntent, downloadUpdate.getNotificationId()),
cancelActionType);
if (!downloadUpdate.getIsOffTheRecord())
builder.setLargeIcon(downloadUpdate.getIcon());
if (downloadUpdate.getIsTransient()) {
builder.setDeleteIntent(buildPendingIntentProvider(
context, cancelIntent, downloadUpdate.getNotificationId()));
}
break;
case DownloadNotificationService.DownloadStatus.COMPLETED:
Preconditions.checkArgument(downloadUpdate.getNotificationId() != -1);
// Don't show file size in incognito mode.
if (downloadUpdate.getTotalBytes() > 0 && !downloadUpdate.getIsOffTheRecord()) {
contentText = context.getResources().getString(
R.string.download_notification_completed_with_size,
DownloadUtils.getStringForBytes(
context, downloadUpdate.getTotalBytes()));
} else {
contentText = context.getResources().getString(
R.string.download_notification_completed);
}
iconId = R.drawable.offline_pin;
if (downloadUpdate.getIsOpenable()) {
Intent intent;
if (LegacyHelpers.isLegacyDownload(downloadUpdate.getContentId())) {
Preconditions.checkNotNull(downloadUpdate.getContentId());
Preconditions.checkArgument(downloadUpdate.getSystemDownloadId() != -1);
intent = new Intent(ACTION_NOTIFICATION_CLICKED);
long[] idArray = {downloadUpdate.getSystemDownloadId()};
intent.putExtra(EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, idArray);
intent.putExtra(EXTRA_DOWNLOAD_FILE_PATH, downloadUpdate.getFilePath());
intent.putExtra(EXTRA_IS_SUPPORTED_MIME_TYPE,
downloadUpdate.getIsSupportedMimeType());
intent.putExtra(
EXTRA_IS_OFF_THE_RECORD, downloadUpdate.getIsOffTheRecord());
intent.putExtra(
EXTRA_DOWNLOAD_CONTENTID_ID, downloadUpdate.getContentId().id);
intent.putExtra(EXTRA_DOWNLOAD_CONTENTID_NAMESPACE,
downloadUpdate.getContentId().namespace);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_ID,
downloadUpdate.getNotificationId());
MediaViewerUtils.setOriginalUrlAndReferralExtraToIntent(intent,
downloadUpdate.getOriginalUrl(), downloadUpdate.getReferrer());
} else {
intent = buildActionIntent(context, ACTION_DOWNLOAD_OPEN,
downloadUpdate.getContentId(), false);
}
ComponentName component = new ComponentName(
context.getPackageName(), DownloadBroadcastManager.class.getName());
intent.setComponent(component);
builder.setContentIntent(PendingIntentProvider.getService(context,
downloadUpdate.getNotificationId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT));
}
// It's the job of the service to ensure that the default icon is provided when
// in incognito mode.
if (downloadUpdate.getIcon() != null)
builder.setLargeIcon(downloadUpdate.getIcon());
break;
case DownloadNotificationService.DownloadStatus.FAILED:
iconId = android.R.drawable.stat_sys_download_done;
contentText = DownloadUtils.getFailStatusString(downloadUpdate.getFailState());
break;
default:
iconId = -1;
contentText = "";
break;
}
Bundle extras = new Bundle();
extras.putInt(EXTRA_NOTIFICATION_BUNDLE_ICON_ID, iconId);
builder.setSmallIcon(iconId).addExtras(extras);
// Context text is shown as title in incognito mode as the file name is not shown.
if (downloadUpdate.getIsOffTheRecord()) {
builder.setContentTitle(contentText);
} else {
builder.setContentText(contentText);
}
// Don't show file name in incognito mode.
if (downloadUpdate.getFileName() != null && !downloadUpdate.getIsOffTheRecord()) {
builder.setContentTitle(DownloadUtils.getAbbreviatedFileName(
downloadUpdate.getFileName(), MAX_FILE_NAME_LENGTH));
}
if (!downloadUpdate.getIsTransient() && downloadUpdate.getNotificationId() != -1
&& downloadStatus != DownloadNotificationService.DownloadStatus.COMPLETED
&& downloadStatus != DownloadNotificationService.DownloadStatus.FAILED) {
Intent downloadHomeIntent = buildActionIntent(
context, ACTION_NOTIFICATION_CLICKED, null, downloadUpdate.getIsOffTheRecord());
builder.setContentIntent(
PendingIntentProvider.getService(context, downloadUpdate.getNotificationId(),
downloadHomeIntent, PendingIntent.FLAG_UPDATE_CURRENT));
}
if (downloadUpdate.getIsOffTheRecord()) {
// A sub text to inform the users that they are using incognito mode.
setSubText(builder,
context.getResources().getString(
R.string.download_notification_incognito_subtext));
} else if (downloadUpdate.getShouldPromoteOrigin()
&& !TextUtils.isEmpty(downloadUpdate.getOriginalUrl())) {
// Always show the origin URL if available (for normal profiles).
String formattedUrl = UrlFormatter.formatUrlForSecurityDisplayOmitScheme(
downloadUpdate.getOriginalUrl());
if (formattedUrl.length() > MAX_ORIGIN_LENGTH) {
// The origin is too long. Strip down to eTLD+1.
formattedUrl = UrlUtilities.getDomainAndRegistry(
downloadUpdate.getOriginalUrl(), false /* includePrivateRegistries */);
}
setSubText(builder, formattedUrl);
}
return builder.build();
}
/**
* Helper method to build a PendingIntent from the provided intent.
* @param intent Intent to broadcast.
* @param notificationId ID of the notification.
*/
private static PendingIntentProvider buildPendingIntentProvider(
Context context, Intent intent, int notificationId) {
return PendingIntentProvider.getService(
context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
/**
* Helper method to set the sub text on different versions of Android.
* @param builder The builder to build notification.
* @param subText A string shown as sub text on the notification.
*/
private static void setSubText(ChromeNotificationBuilder builder, String subText) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setSubText(subText);
} else {
builder.setContentInfo(subText);
}
}
/**
* Helper method to build an download action Intent from the provided information.
* @param context {@link Context} to pull resources from.
* @param action Download action to perform.
* @param id The {@link ContentId} of the download.
* @param isOffTheRecord Whether the download is incognito.
*/
public static Intent buildActionIntent(
Context context, String action, ContentId id, boolean isOffTheRecord) {
ComponentName component = new ComponentName(
context.getPackageName(), DownloadBroadcastManager.class.getName());
Intent intent = new Intent(action);
intent.setComponent(component);
intent.putExtra(EXTRA_DOWNLOAD_CONTENTID_ID, id != null ? id.id : "");
intent.putExtra(EXTRA_DOWNLOAD_CONTENTID_NAMESPACE, id != null ? id.namespace : "");
intent.putExtra(EXTRA_IS_OFF_THE_RECORD, isOffTheRecord);
return intent;
}
}