/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.novoda.downloadmanager.notifications;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.support.v4.util.SimpleArrayMap;
import com.novoda.downloadmanager.lib.DownloadBatch;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Update {@link NotificationManager} to reflect current {@link DownloadBatch} states.
* Collapses similar downloads into a single notification, and builds
* {@link PendingIntent} that launch towards {DownloadReceiver}.
*/
class SynchronisedDownloadNotifier implements DownloadNotifier {
static final int TYPE_ACTIVE = 1;
static final int TYPE_WAITING = 2;
static final int TYPE_SUCCESS = 3;
static final int TYPE_FAILED = 4;
static final int TYPE_CANCELLED = 5;
private final Context context;
/**
* Currently active notifications, mapped from clustering tag to timestamp
* when first shown.
*
* @see #buildNotificationTag(DownloadBatch)
*/
private final SimpleArrayMap<String, Long> activeNotifications = new SimpleArrayMap<>();
private final NotificationDisplayer notificationDisplayer;
public SynchronisedDownloadNotifier(Context context, NotificationDisplayer notificationDisplayer) {
this.context = context;
this.notificationDisplayer = notificationDisplayer;
}
@Override
public void cancelAll() {
notificationDisplayer.cancelAll();
}
/**
* Notify the current speed of an active download, used for calculating
* estimated remaining time.
*/
@Override
public void notifyDownloadSpeed(long id, long bytesPerSecond) {
notificationDisplayer.notifyDownloadSpeed(id, bytesPerSecond);
}
/**
* Update Notifications to reflect the given set of
* {@link DownloadBatch}, adding, collapsing, and removing as needed.
*/
@Override
public void updateWith(Collection<DownloadBatch> batches) {
synchronized (activeNotifications) {
SimpleArrayMap<String, Collection<DownloadBatch>> clusters = getClustersByNotificationTag(batches);
for (int i = 0, size = clusters.size(); i < size; i++) {
String notificationId = clusters.keyAt(i);
long firstShown = getFirstShownTime(notificationId);
notificationDisplayer.buildAndShowNotification(clusters, notificationId, firstShown);
}
List<Integer> staleTagsToBeRemoved = getStaleTagsThatWereNotRenewed(clusters);
notificationDisplayer.cancelStaleTags(staleTagsToBeRemoved);
}
}
private long getFirstShownTime(String notificationId) {
final long firstShown;
if (activeNotifications.containsKey(notificationId)) {
firstShown = activeNotifications.get(notificationId);
} else {
firstShown = System.currentTimeMillis();
activeNotifications.put(notificationId, firstShown);
}
return firstShown;
}
private SimpleArrayMap<String, Collection<DownloadBatch>> getClustersByNotificationTag(Collection<DownloadBatch> batches) {
SimpleArrayMap<String, Collection<DownloadBatch>> clustered = new SimpleArrayMap<>();
for (DownloadBatch batch : batches) {
String tag = buildNotificationTag(batch);
addBatchToCluster(tag, clustered, batch);
}
return clustered;
}
/**
* Build tag used for collapsing several {@link DownloadBatch} into a single
* {@link Notification}.
*/
private String buildNotificationTag(DownloadBatch batch) {
// TODO this method and NotificationDisplayer.#getNotificationTagType have an inherent contract
// If we pulled out a `NotificationTag` value object this would fix it
if (batch.isQueuedForWifi()) {
return TYPE_WAITING + ":" + context.getPackageName();
} else if (batch.isRunning() && batch.shouldShowActiveItem()) {
return TYPE_ACTIVE + ":" + context.getPackageName();
} else if (batch.isError() && !batch.isCancelled() && batch.shouldShowCompletedItem()) {
// Failed downloads always have unique notifications
return TYPE_FAILED + ":" + batch.getBatchId();
} else if (batch.isCancelled() && batch.shouldShowCompletedItem()) {
// Cancelled downloads always have unique notifications
return TYPE_CANCELLED + ":" + batch.getBatchId();
} else if (batch.isSuccess() && batch.shouldShowCompletedItem()) {
// Complete downloads always have unique notifications
return TYPE_SUCCESS + ":" + batch.getBatchId();
} else {
return null;
}
}
private void addBatchToCluster(String tag, SimpleArrayMap<String, Collection<DownloadBatch>> cluster, DownloadBatch batch) {
if (tag == null) {
return;
}
Collection<DownloadBatch> batches;
if (cluster.containsKey(tag)) {
batches = cluster.get(tag);
} else {
batches = new ArrayList<>();
cluster.put(tag, batches);
}
batches.add(batch);
}
private List<Integer> getStaleTagsThatWereNotRenewed(SimpleArrayMap<String, Collection<DownloadBatch>> clustered) {
List<Integer> staleTags = new ArrayList<>();
for (int i = activeNotifications.size() - 1; i >= 0; i--) {
String tag = activeNotifications.keyAt(i);
if (!clustered.containsKey(tag)) {
staleTags.add(tag.hashCode());
activeNotifications.removeAt(i);
}
}
return staleTags;
}
}