Index: chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationService.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationService.java b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationService.java |
index 979b2ba628be529f56c999ee86db0770b2dea8c2..d3da69c8270e8c4783452f99501b4bf034ae8d3e 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationService.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationService.java |
@@ -23,9 +23,11 @@ import android.graphics.Rect; |
import android.graphics.drawable.shapes.OvalShape; |
import android.os.Binder; |
import android.os.Build; |
+import android.os.Bundle; |
import android.os.IBinder; |
import android.service.notification.StatusBarNotification; |
import android.text.TextUtils; |
+import android.util.Pair; |
import org.chromium.base.ApiCompatibilityUtils; |
import org.chromium.base.ApplicationStatus; |
@@ -59,7 +61,7 @@ import java.util.concurrent.TimeUnit; |
* |
* On O and above, this service will receive {@link Service#startForeground(int, Notification)} |
* calls when containing active downloads. The foreground notification will be the summary |
- * notification generated by {@link DownloadNotificationService#getSummaryNotification(Context)}. |
+ * notification generated by {@link DownloadNotificationService#buildSummaryNotification(Context)}. |
* The service will receive a {@link Service#stopForeground(boolean)} call when all active downloads |
* are paused. The summary notification will be hidden when there are no other notifications in the |
* {@link NotificationConstants#GROUP_DOWNLOADS} group. This gets checked after every notification |
@@ -93,6 +95,8 @@ public class DownloadNotificationService extends Service { |
/** Notification Id starting value, to avoid conflicts from IDs used in prior versions. */ |
+ private static final String EXTRA_NOTIFICATION_BUNDLE_ICON_ID = |
+ "Chrome.NotificationBundleIconIdExtra"; |
private static final int STARTING_NOTIFICATION_ID = 1000000; |
private static final int MAX_RESUMPTION_ATTEMPT_LEFT = 5; |
@VisibleForTesting static final long SECONDS_PER_MINUTE = TimeUnit.MINUTES.toSeconds(1); |
@@ -145,7 +149,7 @@ public class DownloadNotificationService extends Service { |
intent.setComponent(new ComponentName(context, DownloadNotificationService.class)); |
if (BuildInfo.isAtLeastO()) { |
- Notification notification = getSummaryNotification(context); |
+ Notification notification = buildSummaryNotification(context); |
AppHooks.get().startServiceWithNotification( |
intent, NotificationConstants.NOTIFICATION_ID_DOWNLOAD_SUMMARY, notification); |
@@ -155,16 +159,101 @@ public class DownloadNotificationService extends Service { |
} |
/** |
- * Builds a summary notification that represents downloads. This is the notification passed to |
- * {@link #startForeground(int, Notification)}, which keeps this service in the foreground. |
- * @param context The context used to build the notification and pull specific resources. |
- * @return The {@link Notification} to show for the summary. Meant to be used by |
- * {@link NotificationManager#notify(int, Notification)}. |
+ * Calculates the suggested icon for the summary notification based on the other notifications |
+ * currently showing. |
+ * @param context A context to use to query Android-specific information (NotificationManager). |
+ * @param removedNotificationId The id of a notification that is currently closing and should be |
+ * ignored. -1 if no notifications are being closed. |
+ * @param addedNotification A {@link Pair} of <id, Notification> of a notification that is |
+ * currently being added and should be used in addition to or in |
+ * place of the existing icons. |
+ * @return A {@link Pair} that represents both whether or not the new icon |
+ * is different from the old one and the icon id itself. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.M) |
+ private static Pair<Boolean, Integer> getSummaryIcon(Context context, int removedNotificationId, |
+ Pair<Integer, Notification> addedNotification) { |
+ if (!BuildInfo.isAtLeastO()) return new Pair<Boolean, Integer>(false, -1); |
+ boolean progress = false; |
+ boolean paused = false; |
+ boolean pending = false; |
+ boolean completed = false; |
+ boolean failed = false; |
+ |
+ final int progressIcon = android.R.drawable.stat_sys_download; |
+ final int pausedIcon = R.drawable.ic_download_pause; |
+ final int pendingIcon = R.drawable.ic_download_pending; |
+ final int completedIcon = R.drawable.offline_pin; |
+ final int failedIcon = android.R.drawable.stat_sys_download_done; |
+ |
+ NotificationManager manager = |
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); |
+ StatusBarNotification[] notifications = manager.getActiveNotifications(); |
+ |
+ int oldIcon = -1; |
+ for (StatusBarNotification notification : notifications) { |
+ boolean isDownloadsGroup = TextUtils.equals(notification.getNotification().getGroup(), |
+ NotificationConstants.GROUP_DOWNLOADS); |
+ if (!isDownloadsGroup) continue; |
+ if (notification.getId() == removedNotificationId) continue; |
+ |
+ boolean isSummaryNotification = |
+ notification.getId() == NotificationConstants.NOTIFICATION_ID_DOWNLOAD_SUMMARY; |
+ |
+ if (addedNotification != null && addedNotification.first == notification.getId()) |
+ continue; |
+ |
+ int icon = |
+ notification.getNotification().extras.getInt(EXTRA_NOTIFICATION_BUNDLE_ICON_ID); |
+ if (isSummaryNotification) { |
+ oldIcon = icon; |
+ continue; |
+ } |
+ |
+ progress |= icon == progressIcon; |
+ paused |= icon == pausedIcon; |
+ pending |= icon == pendingIcon; |
+ completed |= icon == completedIcon; |
+ failed |= icon == failedIcon; |
+ } |
+ |
+ if (addedNotification != null) { |
+ int icon = addedNotification.second.extras.getInt(EXTRA_NOTIFICATION_BUNDLE_ICON_ID); |
+ |
+ progress |= icon == progressIcon; |
+ paused |= icon == pausedIcon; |
+ pending |= icon == pendingIcon; |
+ completed |= icon == completedIcon; |
+ failed |= icon == failedIcon; |
+ } |
+ |
+ int newIcon = android.R.drawable.stat_sys_download_done; |
+ if (progress) { |
+ newIcon = android.R.drawable.stat_sys_download; |
+ } else if (paused) { |
+ newIcon = R.drawable.ic_download_pause; |
+ } else if (pending) { |
+ newIcon = R.drawable.ic_download_pending; |
+ } else if (completed) { |
+ newIcon = R.drawable.offline_pin; |
+ } else if (failed) { |
+ newIcon = android.R.drawable.stat_sys_download_done; |
+ } |
+ return new Pair<Boolean, Integer>(newIcon != oldIcon, newIcon); |
+ } |
+ |
+ /** |
+ * Builds a summary notification that represents all downloads. |
+ * {@see #buildSummaryNotification(Context)}. |
+ * @param context A context used to query Android strings and resources. |
+ * @param iconId The id of an icon to use for the notification. |
+ * @return a {@link Notification} that represents the summary icon for all downloads. |
*/ |
- private static Notification getSummaryNotification(Context context) { |
+ private static Notification buildSummaryNotificationWithIcon(Context context, int iconId) { |
String title = |
context.getResources().getString(R.string.download_notification_summary_title, |
DownloadSharedPreferenceHelper.getInstance().getEntries().size()); |
+ |
ChromeNotificationBuilder builder = |
AppHooks.get() |
.createChromeNotificationBuilder(true /* preferCompat */, |
@@ -173,10 +262,13 @@ public class DownloadNotificationService extends Service { |
NotificationConstants.CATEGORY_GROUP_ID_GENERAL, |
context.getString(R.string.notification_category_group_general)) |
.setContentTitle(title) |
- .setSmallIcon(android.R.drawable.stat_sys_download_done) |
+ .setSmallIcon(iconId) |
.setLocalOnly(true) |
.setGroup(NotificationConstants.GROUP_DOWNLOADS) |
.setGroupSummary(true); |
+ Bundle extras = new Bundle(); |
+ extras.putInt(EXTRA_NOTIFICATION_BUNDLE_ICON_ID, iconId); |
+ builder.addExtras(extras); |
// This notification should not actually be shown. But if it is, set the click intent to |
// open downloads home. |
@@ -190,6 +282,18 @@ public class DownloadNotificationService extends Service { |
} |
/** |
+ * Builds a summary notification that represents downloads. This is the notification passed to |
+ * {@link #startForeground(int, Notification)}, which keeps this service in the foreground. |
+ * @param context The context used to build the notification and pull specific resources. |
+ * @return The {@link Notification} to show for the summary. Meant to be used by |
+ * {@link NotificationManager#notify(int, Notification)}. |
+ */ |
+ private static Notification buildSummaryNotification(Context context) { |
+ Pair<Boolean, Integer> icon = getSummaryIcon(context, -1, null); |
+ return buildSummaryNotificationWithIcon(context, icon.second); |
+ } |
+ |
+ /** |
* @return Whether or not there are any current resumable downloads being tracked. These |
* tracked downloads may not currently be showing notifications. |
*/ |
@@ -305,12 +409,12 @@ public class DownloadNotificationService extends Service { |
/** |
* On >= O Android releases, puts this service into a foreground state, binding it to the |
- * {@link Notification} generated by {@link #getSummaryNotification(Context)}. |
+ * {@link Notification} generated by {@link #buildSummaryNotification(Context)}. |
*/ |
@VisibleForTesting |
void startForegroundInternal() { |
if (!BuildInfo.isAtLeastO()) return; |
- Notification notification = getSummaryNotification(getApplicationContext()); |
+ Notification notification = buildSummaryNotification(getApplicationContext()); |
startForeground(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_SUMMARY, notification); |
} |
@@ -441,6 +545,30 @@ public class DownloadNotificationService extends Service { |
} |
/** |
+ * Updates the notification summary with a new icon, if necessary. |
+ * @param removedNotificationId The id of a notification that is currently closing and should be |
+ * ignored. -1 if no notifications are being closed. |
+ * @param addedNotification A {@link Pair} of <id, Notification> of a notification that is |
+ * currently being added and should be used in addition to or in |
+ * place of the existing icons. |
+ */ |
+ @VisibleForTesting |
+ void updateSummaryIcon( |
+ int removedNotificationId, Pair<Integer, Notification> addedNotification) { |
+ if (!BuildInfo.isAtLeastO()) return; |
+ |
+ Pair<Boolean, Integer> icon = |
+ getSummaryIcon(mContext, removedNotificationId, addedNotification); |
+ |
+ // Avoid rebuilding the summary notification if the icon hasn't changed or we have (or are |
+ // about to have) no active downloads. |
+ if (!icon.first || !hasDownloadNotifications(removedNotificationId)) return; |
+ |
+ mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_SUMMARY, |
+ buildSummaryNotificationWithIcon(mContext, icon.second)); |
+ } |
+ |
+ /** |
* Cancels the existing summary notification. Moved to a helper method for test mocking. |
*/ |
@VisibleForTesting |
@@ -460,11 +588,11 @@ public class DownloadNotificationService extends Service { |
* Or pass {@code null} if no notification fits that criteria. |
*/ |
@TargetApi(Build.VERSION_CODES.M) |
- void hideSummaryNotificationIfNecessary(Integer notificationIdToIgnore) { |
- if (!BuildInfo.isAtLeastO()) return; |
- if (mDownloadsInProgress.size() > 0) return; |
+ boolean hideSummaryNotificationIfNecessary(Integer notificationIdToIgnore) { |
+ if (!BuildInfo.isAtLeastO()) return false; |
+ if (mDownloadsInProgress.size() > 0) return false; |
- if (hasDownloadNotifications(notificationIdToIgnore)) return; |
+ if (hasDownloadNotifications(notificationIdToIgnore)) return false; |
StatusBarNotification notification = getSummaryNotification(); |
if (notification != null) { |
@@ -495,6 +623,7 @@ public class DownloadNotificationService extends Service { |
// Stop the service which should start the destruction process. At this point we should be |
// (1) a background service and (2) unbound from any clients. |
stopSelf(); |
+ return true; |
} |
@Override |
@@ -654,7 +783,9 @@ public class DownloadNotificationService extends Service { |
// down, don't stop foreground if we have no download notifications left to show. Hiding |
// the summary will take care of that for us. |
stopTrackingInProgressDownload(downloadGuid, hasDownloadNotifications(notificationId)); |
- hideSummaryNotificationIfNecessary(notificationId); |
+ if (!hideSummaryNotificationIfNecessary(notificationId)) { |
+ updateSummaryIcon(notificationId, null); |
+ } |
} |
/** |
@@ -834,6 +965,9 @@ public class DownloadNotificationService extends Service { |
*/ |
private ChromeNotificationBuilder buildNotification( |
int iconId, String title, String contentText) { |
+ Bundle extras = new Bundle(); |
+ extras.putInt(EXTRA_NOTIFICATION_BUNDLE_ICON_ID, iconId); |
+ |
ChromeNotificationBuilder builder = |
AppHooks.get() |
.createChromeNotificationBuilder(true /* preferCompat */, |
@@ -847,7 +981,8 @@ public class DownloadNotificationService extends Service { |
.setLocalOnly(true) |
.setAutoCancel(true) |
.setContentText(contentText) |
- .setGroup(NotificationConstants.GROUP_DOWNLOADS); |
+ .setGroup(NotificationConstants.GROUP_DOWNLOADS) |
+ .addExtras(extras); |
return builder; |
} |
@@ -1061,6 +1196,7 @@ public class DownloadNotificationService extends Service { |
} else { |
mDownloadSharedPreferenceHelper.removeSharedPreferenceEntry(downloadGuid); |
} |
+ updateSummaryIcon(-1, new Pair<Integer, Notification>(id, notification)); |
} |
private void trackNotificationUma(boolean isOfflinePage, String downloadGuid) { |