package com.novoda.downloadmanager.notifications; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import android.support.v4.app.NotificationCompat; import android.support.v4.util.LongSparseArray; import android.support.v4.util.SimpleArrayMap; import android.text.TextUtils; import android.text.format.DateUtils; import com.novoda.downloadmanager.Download; import com.novoda.downloadmanager.R; import com.novoda.downloadmanager.lib.DownloadBatch; import com.novoda.downloadmanager.lib.DownloadManager; import com.novoda.downloadmanager.lib.DownloadReceiver; import com.novoda.downloadmanager.lib.DownloadStatus; import com.novoda.downloadmanager.lib.PublicFacingDownloadMarshaller; import com.novoda.downloadmanager.lib.PublicFacingStatusTranslator; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; public class NotificationDisplayer { public static final String ACTION_NOTIFICATION_DISMISSED = "com.novoda.downloadmanager.action.NOTIFICATION_DISMISSED"; public static final String ACTION_DOWNLOAD_FAILED_CLICK = "com.novoda.downloadmanager.action.DOWNLOAD_FAILED_CLICK"; public static final String ACTION_DOWNLOAD_RUNNING_CLICK = "com.novoda.downloadmanager.action.DOWNLOAD_RUNNING_CLICK"; public static final String ACTION_DOWNLOAD_SUCCESS_CLICK = "com.novoda.downloadmanager.action.DOWNLOAD_SUCCESS_CLICK"; public static final String ACTION_DOWNLOAD_CANCELLED_CLICK = "com.novoda.downloadmanager.action.DOWNLOAD_CANCELLED_CLICK"; public static final String ACTION_DOWNLOAD_SUBMITTED_CLICK = "com.novoda.downloadmanager.action.DOWNLOAD_SUBMITTED_CLICK"; public static final String ACTION_DOWNLOAD_OTHER_CLICK = "com.novoda.downloadmanager.action.DOWNLOAD_OTHER_CLICK"; private final Context context; private final NotificationManager notificationManager; private final NotificationImageRetriever imageRetriever; private final Resources resources; /** * Current speed of active downloads, mapped from {@link DownloadBatch#batchId} * to speed in bytes per second. */ private final LongSparseArray<Long> downloadSpeed = new LongSparseArray<>(); private final NotificationCustomiser notificationCustomiser; private final PublicFacingStatusTranslator statusTranslator; private final PublicFacingDownloadMarshaller downloadMarshaller; public NotificationDisplayer( Context context, NotificationManager notificationManager, NotificationImageRetriever imageRetriever, Resources resources, NotificationCustomiser notificationCustomiser, PublicFacingStatusTranslator statusTranslator, PublicFacingDownloadMarshaller downloadMarshaller) { this.context = context; this.notificationManager = notificationManager; this.imageRetriever = imageRetriever; this.resources = resources; this.notificationCustomiser = notificationCustomiser; this.statusTranslator = statusTranslator; this.downloadMarshaller = downloadMarshaller; } public void buildAndShowNotification(SimpleArrayMap<String, Collection<DownloadBatch>> clusters, String notificationId, long firstShown) { int type = getNotificationTagType(notificationId); Collection<DownloadBatch> cluster = clusters.get(notificationId); NotificationCompat.Builder builder = new NotificationCompat.Builder(context); builder.setWhen(firstShown); buildIcon(type, builder); buildActionIntents(type, cluster, builder); Notification notification = buildTitlesAndDescription(type, cluster, builder); notificationManager.notify(notificationId.hashCode(), notification); } /** * Return the cluster type of the given as created by * {@link SynchronisedDownloadNotifier#buildNotificationTag(DownloadBatch)}. */ private int getNotificationTagType(String tag) { return Integer.parseInt(tag.substring(0, tag.indexOf(':'))); } private void buildIcon(int type, NotificationCompat.Builder builder) { switch (type) { case SynchronisedDownloadNotifier.TYPE_ACTIVE: builder.setSmallIcon(android.R.drawable.stat_sys_download); break; case SynchronisedDownloadNotifier.TYPE_WAITING: case SynchronisedDownloadNotifier.TYPE_FAILED: builder.setSmallIcon(android.R.drawable.stat_sys_warning); break; case SynchronisedDownloadNotifier.TYPE_SUCCESS: builder.setSmallIcon(android.R.drawable.stat_sys_download_done); break; default: builder.setSmallIcon(android.R.drawable.stat_sys_warning); break; } } private void buildActionIntents(int type, Collection<DownloadBatch> cluster, NotificationCompat.Builder builder) { DownloadBatch batch = cluster.iterator().next(); long batchId = batch.getBatchId(); int batchStatus = batch.getStatus(); if (type == SynchronisedDownloadNotifier.TYPE_ACTIVE || type == SynchronisedDownloadNotifier.TYPE_WAITING) { builder.setOngoing(true); } else if (type == SynchronisedDownloadNotifier.TYPE_SUCCESS || type == SynchronisedDownloadNotifier.TYPE_CANCELLED || type == SynchronisedDownloadNotifier.TYPE_FAILED) { Intent dismissedIntent = createNotificationDismissedIntent(batchId); builder.setDeleteIntent(PendingIntent.getBroadcast(context, 0, dismissedIntent, 0)); builder.setAutoCancel(true); } Intent clickIntent = createClickIntent(batchId, batchStatus); builder.setContentIntent(PendingIntent.getBroadcast(context, 0, clickIntent, PendingIntent.FLAG_CANCEL_CURRENT)); customiseNotification(type, builder, batch); } private Intent createNotificationDismissedIntent(long batchId) { Intent hideIntent = new Intent(ACTION_NOTIFICATION_DISMISSED, Uri.EMPTY, context, DownloadReceiver.class); hideIntent.putExtra(DownloadReceiver.EXTRA_BATCH_ID, batchId); hideIntent.setData(createUniqueUri(hideIntent)); return hideIntent; } private Uri createUniqueUri(Intent intent) { return Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)); } private Intent createClickIntent(long batchId, int batchStatus) { String action = getActionFrom(batchStatus); Intent clickIntent = new Intent(action, Uri.EMPTY, context, DownloadReceiver.class); clickIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS, new long[]{batchId}); clickIntent.putExtra(DownloadReceiver.EXTRA_BATCH_ID, batchId); int status = statusTranslator.translate(batchStatus); clickIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_STATUSES, new int[]{status}); clickIntent.setData(createUniqueUri(clickIntent)); return clickIntent; } private String getActionFrom(int batchStatus) { if (DownloadStatus.isFailure(batchStatus)) { return ACTION_DOWNLOAD_FAILED_CLICK; } else if (DownloadStatus.isRunning(batchStatus)) { return ACTION_DOWNLOAD_RUNNING_CLICK; } else if (DownloadStatus.isSuccess(batchStatus)) { return ACTION_DOWNLOAD_SUCCESS_CLICK; } else if (DownloadStatus.isCancelled(batchStatus)) { return ACTION_DOWNLOAD_CANCELLED_CLICK; } else if (DownloadStatus.isSubmitted(batchStatus)) { return ACTION_DOWNLOAD_SUBMITTED_CLICK; } return ACTION_DOWNLOAD_OTHER_CLICK; } private void customiseNotification(int type, NotificationCompat.Builder builder, DownloadBatch batch) { Download download = downloadMarshaller.marshall(batch); switch (type) { case SynchronisedDownloadNotifier.TYPE_WAITING: notificationCustomiser.customiseQueued(download, builder); break; case SynchronisedDownloadNotifier.TYPE_ACTIVE: notificationCustomiser.customiseDownloading(download, builder); break; case SynchronisedDownloadNotifier.TYPE_SUCCESS: notificationCustomiser.customiseComplete(download, builder); break; case SynchronisedDownloadNotifier.TYPE_CANCELLED: notificationCustomiser.customiseCancelled(download, builder); break; case SynchronisedDownloadNotifier.TYPE_FAILED: notificationCustomiser.customiseFailed(download, builder); break; default: throw new IllegalStateException("Deal with this new type " + type); } } private Notification buildTitlesAndDescription(int type, Collection<DownloadBatch> cluster, NotificationCompat.Builder builder) { String remainingText = null; String percentText = null; if (type == SynchronisedDownloadNotifier.TYPE_ACTIVE) { int totalPercent = 0; long remainingMillis = 0; synchronized (downloadSpeed) { for (DownloadBatch batch : cluster) { DownloadBatch.Statistics statistics = batch.getLiveStatistics(downloadSpeed); totalPercent += statistics.getPercentComplete(); remainingMillis += statistics.getTimeRemaining(); } totalPercent /= cluster.size(); } if (totalPercent > 0) { percentText = context.getString(R.string.dl__download_percent, totalPercent); if (remainingMillis > 0) { remainingText = context.getString(R.string.dl__duration, formatDuration(remainingMillis)); } builder.setProgress(100, totalPercent, false); } else { builder.setProgress(100, 0, true); } } List<DownloadBatch> currentBatches = new ArrayList<>(); for (DownloadBatch batch : cluster) { currentBatches.add(batch); } if (currentBatches.size() == 1) { DownloadBatch batch = currentBatches.iterator().next(); return buildSingleNotification(type, builder, batch, percentText); } else { return buildStackedNotification(type, builder, currentBatches, remainingText, percentText); } } private Notification buildSingleNotification( int type, NotificationCompat.Builder builder, DownloadBatch batch, String percentText) { NotificationCompat.BigPictureStyle style = new NotificationCompat.BigPictureStyle(); String imageUrl = batch.getBigPictureUrl(); if (!TextUtils.isEmpty(imageUrl)) { Bitmap bitmap = imageRetriever.retrieveImage(imageUrl); style.bigPicture(bitmap); } CharSequence title = getDownloadTitle(batch); builder.setContentTitle(title); style.setBigContentTitle(title); if (type == SynchronisedDownloadNotifier.TYPE_ACTIVE) { String description = batch.getDescription(); if (TextUtils.isEmpty(description)) { setSecondaryNotificationText(builder, style, context.getString(R.string.dl__downloading)); } else { setSecondaryNotificationText(builder, style, description); } builder.setContentInfo(percentText); } else if (type == SynchronisedDownloadNotifier.TYPE_WAITING) { setSecondaryNotificationText(builder, style, context.getString(R.string.dl__download_size_requires_wifi)); } else if (type == SynchronisedDownloadNotifier.TYPE_SUCCESS) { setSecondaryNotificationText(builder, style, context.getString(R.string.dl__download_complete)); } else if (type == SynchronisedDownloadNotifier.TYPE_FAILED) { setSecondaryNotificationText(builder, style, context.getString(R.string.dl__download_unsuccessful)); } else if (type == SynchronisedDownloadNotifier.TYPE_CANCELLED) { setSecondaryNotificationText(builder, style, context.getString(R.string.dl__download_cancelled)); } if (!TextUtils.isEmpty(imageUrl)) { builder.setStyle(style); } return builder.build(); } private CharSequence getDownloadTitle(DownloadBatch batch) { String title = batch.getTitle(); if (TextUtils.isEmpty(title)) { return context.getString(R.string.dl__title_unknown); } else { return title; } } private void setSecondaryNotificationText(NotificationCompat.Builder builder, NotificationCompat.BigPictureStyle style, String description) { builder.setContentText(description); style.setSummaryText(description); } private Notification buildStackedNotification( int type, NotificationCompat.Builder builder, Collection<DownloadBatch> currentBatches, String remainingText, String percentText) { final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(builder); for (DownloadBatch batch : currentBatches) { inboxStyle.addLine(getDownloadTitle(batch)); } if (type == SynchronisedDownloadNotifier.TYPE_ACTIVE) { builder.setContentTitle(resources.getQuantityString(R.plurals.dl__notif_summary_active, currentBatches.size(), currentBatches.size())); builder.setContentInfo(percentText); setSecondaryNotificationText(builder, inboxStyle, remainingText); } else if (type == SynchronisedDownloadNotifier.TYPE_WAITING) { builder.setContentTitle(resources.getQuantityString(R.plurals.dl__notif_summary_waiting, currentBatches.size(), currentBatches.size())); setSecondaryNotificationText(builder, inboxStyle, context.getString(R.string.dl__download_size_requires_wifi)); } else if (type == SynchronisedDownloadNotifier.TYPE_SUCCESS) { setSecondaryNotificationText(builder, inboxStyle, context.getString(R.string.dl__download_complete)); } else if (type == SynchronisedDownloadNotifier.TYPE_FAILED) { setSecondaryNotificationText(builder, inboxStyle, context.getString(R.string.dl__download_unsuccessful)); } else if (type == SynchronisedDownloadNotifier.TYPE_CANCELLED) { setSecondaryNotificationText(builder, inboxStyle, context.getString(R.string.dl__download_cancelled)); } return inboxStyle.build(); } private void setSecondaryNotificationText(NotificationCompat.Builder builder, NotificationCompat.InboxStyle style, String description) { builder.setContentText(description); style.setSummaryText(description); } public void notifyDownloadSpeed(long id, long bytesPerSecond) { synchronized (downloadSpeed) { if (bytesPerSecond != 0) { downloadSpeed.put(id, bytesPerSecond); } else { downloadSpeed.remove(id); } } } /** * Return given duration in a human-friendly format. For example, "4 * minutes" or "1 second". Returns only largest meaningful unit of time, * from seconds up to hours. */ private CharSequence formatDuration(long millis) { if (millis >= DateUtils.HOUR_IN_MILLIS) { int hours = (int) TimeUnit.MILLISECONDS.toHours(millis + TimeUnit.MINUTES.toMillis(30)); return resources.getQuantityString(R.plurals.dl__duration_hours, hours, hours); } else if (millis >= DateUtils.MINUTE_IN_MILLIS) { int minutes = (int) TimeUnit.MILLISECONDS.toMinutes(millis + TimeUnit.SECONDS.toMillis(30)); return resources.getQuantityString(R.plurals.dl__duration_minutes, minutes, minutes); } else { int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(millis + 500); return resources.getQuantityString(R.plurals.dl__duration_seconds, seconds, seconds); } } public void cancelStaleTags(List<Integer> staleTagsToBeRemoved) { for (Integer tag : staleTagsToBeRemoved) { notificationManager.cancel(tag); } } public void cancelAll() { notificationManager.cancelAll(); } }