1a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey/*
2a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Copyright (C) 2012 The Android Open Source Project
3a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *
4a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Licensed under the Apache License, Version 2.0 (the "License");
5a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * you may not use this file except in compliance with the License.
6a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * You may obtain a copy of the License at
7a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *
8a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *      http://www.apache.org/licenses/LICENSE-2.0
9a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey *
10a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Unless required by applicable law or agreed to in writing, software
11a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * distributed under the License is distributed on an "AS IS" BASIS,
12a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * See the License for the specific language governing permissions and
14a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * limitations under the License.
15a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey */
16a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
17a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeypackage com.android.providers.downloads;
18a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
19a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport static android.app.DownloadManager.Request.VISIBILITY_VISIBLE;
20a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
21a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
221ad10ce731d1b54692d7d5ee32601e965f503fa4Jeff Sharkeyimport static android.provider.Downloads.Impl.STATUS_RUNNING;
23703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkeyimport static com.android.providers.downloads.Constants.TAG;
24a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
25a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.DownloadManager;
26a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.Notification;
27a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.NotificationManager;
28a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.app.PendingIntent;
29a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.ContentUris;
30a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.Context;
31a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.Intent;
32a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.content.res.Resources;
33a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.net.Uri;
34703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkeyimport android.os.SystemClock;
35a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.provider.Downloads;
36a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport android.text.TextUtils;
3752b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkeyimport android.text.format.DateUtils;
38703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkeyimport android.util.Log;
3938648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkeyimport android.util.LongSparseLongArray;
40a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
41a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport com.google.common.collect.ArrayListMultimap;
42a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport com.google.common.collect.Maps;
43a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport com.google.common.collect.Multimap;
44a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
45250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughesimport java.text.NumberFormat;
46a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport java.util.Collection;
47a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport java.util.HashMap;
48a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport java.util.Iterator;
49a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
50a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeyimport javax.annotation.concurrent.GuardedBy;
51a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
52a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey/**
53a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * Update {@link NotificationManager} to reflect current {@link DownloadInfo}
54a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * states. Collapses similar downloads into a single notification, and builds
55a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey * {@link PendingIntent} that launch towards {@link DownloadReceiver}.
56a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey */
57a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkeypublic class DownloadNotifier {
58a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
59a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static final int TYPE_ACTIVE = 1;
60a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static final int TYPE_WAITING = 2;
61a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static final int TYPE_COMPLETE = 3;
62a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
63a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private final Context mContext;
64a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private final NotificationManager mNotifManager;
65a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
66a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
67a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * Currently active notifications, mapped from clustering tag to timestamp
68a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * when first shown.
69a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     *
70a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * @see #buildNotificationTag(DownloadInfo)
71a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
72a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    @GuardedBy("mActiveNotifs")
73a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private final HashMap<String, Long> mActiveNotifs = Maps.newHashMap();
74a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
7538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    /**
7638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     * Current speed of active downloads, mapped from {@link DownloadInfo#mId}
7738648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     * to speed in bytes per second.
7838648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     */
7938648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    @GuardedBy("mDownloadSpeed")
8038648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    private final LongSparseLongArray mDownloadSpeed = new LongSparseLongArray();
8138648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey
82703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    /**
83703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     * Last time speed was reproted, mapped from {@link DownloadInfo#mId} to
84703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     * {@link SystemClock#elapsedRealtime()}.
85703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     */
86703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    @GuardedBy("mDownloadSpeed")
87703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    private final LongSparseLongArray mDownloadTouch = new LongSparseLongArray();
88703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey
89a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    public DownloadNotifier(Context context) {
90a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        mContext = context;
91a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        mNotifManager = (NotificationManager) context.getSystemService(
92a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                Context.NOTIFICATION_SERVICE);
93a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
94a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
95b0bb182a1b52b31ccf1ec5e0be82308ebb4857e2Jeff Sharkey    public void cancelAll() {
96b0bb182a1b52b31ccf1ec5e0be82308ebb4857e2Jeff Sharkey        mNotifManager.cancelAll();
97b0bb182a1b52b31ccf1ec5e0be82308ebb4857e2Jeff Sharkey    }
98b0bb182a1b52b31ccf1ec5e0be82308ebb4857e2Jeff Sharkey
99a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
100703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey     * Notify the current speed of an active download, used for calculating
10138648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     * estimated remaining time.
10238648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey     */
10338648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    public void notifyDownloadSpeed(long id, long bytesPerSecond) {
10438648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey        synchronized (mDownloadSpeed) {
10538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey            if (bytesPerSecond != 0) {
10638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                mDownloadSpeed.put(id, bytesPerSecond);
107703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                mDownloadTouch.put(id, SystemClock.elapsedRealtime());
10838648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey            } else {
10938648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                mDownloadSpeed.delete(id);
110703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                mDownloadTouch.delete(id);
11138648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey            }
11238648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey        }
11338648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    }
11438648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey
11538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey    /**
116a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * Update {@link NotificationManager} to reflect the given set of
117a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * {@link DownloadInfo}, adding, collapsing, and removing as needed.
118a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
119a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    public void updateWith(Collection<DownloadInfo> downloads) {
120a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        synchronized (mActiveNotifs) {
121a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            updateWithLocked(downloads);
122a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
123a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
124a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
125a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private void updateWithLocked(Collection<DownloadInfo> downloads) {
126a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        final Resources res = mContext.getResources();
127a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
128a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        // Cluster downloads together
129a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        final Multimap<String, DownloadInfo> clustered = ArrayListMultimap.create();
130a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        for (DownloadInfo info : downloads) {
131a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final String tag = buildNotificationTag(info);
132a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (tag != null) {
133a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                clustered.put(tag, info);
134a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
135a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
136a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
137a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        // Build notification for each cluster
138a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        for (String tag : clustered.keySet()) {
139a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final int type = getNotificationTagType(tag);
140a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final Collection<DownloadInfo> cluster = clustered.get(tag);
141a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
142a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final Notification.Builder builder = new Notification.Builder(mContext);
14352d35ed20edec2b5086459f1d5d64309c421f3b2Selim Cinek            builder.setColor(res.getColor(
14452d35ed20edec2b5086459f1d5d64309c421f3b2Selim Cinek                    com.android.internal.R.color.system_notification_accent_color));
145a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
146a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Use time when cluster was first shown to avoid shuffling
147a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final long firstShown;
148a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (mActiveNotifs.containsKey(tag)) {
149a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                firstShown = mActiveNotifs.get(tag);
150a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else {
151a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                firstShown = System.currentTimeMillis();
152a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                mActiveNotifs.put(tag, firstShown);
153a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
154a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            builder.setWhen(firstShown);
155a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
156a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Show relevant icon
157a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (type == TYPE_ACTIVE) {
158a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setSmallIcon(android.R.drawable.stat_sys_download);
159a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else if (type == TYPE_WAITING) {
160a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setSmallIcon(android.R.drawable.stat_sys_warning);
161a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else if (type == TYPE_COMPLETE) {
162a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setSmallIcon(android.R.drawable.stat_sys_download_done);
163a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
164a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
165a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Build action intents
166a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (type == TYPE_ACTIVE || type == TYPE_WAITING) {
167af49093b1554fdc55977e9281505e05865f33813Danny Baumann                // build a synthetic uri for intent identification purposes
168af49093b1554fdc55977e9281505e05865f33813Danny Baumann                final Uri uri = new Uri.Builder().scheme("active-dl").appendPath(tag).build();
169a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Intent intent = new Intent(Constants.ACTION_LIST,
170af49093b1554fdc55977e9281505e05865f33813Danny Baumann                        uri, mContext, DownloadReceiver.class);
171a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
172a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        getDownloadIds(cluster));
173af49093b1554fdc55977e9281505e05865f33813Danny Baumann                builder.setContentIntent(PendingIntent.getBroadcast(mContext,
174af49093b1554fdc55977e9281505e05865f33813Danny Baumann                        0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
175a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setOngoing(true);
176a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
177a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else if (type == TYPE_COMPLETE) {
178a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final DownloadInfo info = cluster.iterator().next();
179a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Uri uri = ContentUris.withAppendedId(
180a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, info.mId);
18164a9e6cb3628db724e04492cf207757348960eb7Jeff Sharkey                builder.setAutoCancel(true);
182a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
183a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final String action;
184a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (Downloads.Impl.isStatusError(info.mStatus)) {
185a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    action = Constants.ACTION_LIST;
186a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else {
187a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    if (info.mDestination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
188a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        action = Constants.ACTION_OPEN;
189a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    } else {
190a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        action = Constants.ACTION_LIST;
191a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
192a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
193a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
194a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Intent intent = new Intent(action, uri, mContext, DownloadReceiver.class);
195a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
196a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        getDownloadIds(cluster));
197af49093b1554fdc55977e9281505e05865f33813Danny Baumann                builder.setContentIntent(PendingIntent.getBroadcast(mContext,
198af49093b1554fdc55977e9281505e05865f33813Danny Baumann                        0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
199a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
200a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Intent hideIntent = new Intent(Constants.ACTION_HIDE,
201a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        uri, mContext, DownloadReceiver.class);
202a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, hideIntent, 0));
203a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
204a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
205a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Calculate and show progress
206a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            String remainingText = null;
207a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            String percentText = null;
208a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (type == TYPE_ACTIVE) {
209a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                long current = 0;
210a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                long total = 0;
211fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey                long speed = 0;
21238648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                synchronized (mDownloadSpeed) {
21338648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                    for (DownloadInfo info : cluster) {
21438648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                        if (info.mTotalBytes != -1) {
21538648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                            current += info.mCurrentBytes;
21638648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                            total += info.mTotalBytes;
21738648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                            speed += mDownloadSpeed.get(info.mId);
21838648831a92295e9a11831e19e5a9dab4cbd939eJeff Sharkey                        }
219a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
220a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
221a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
222a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (total > 0) {
223250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughes                    percentText =
224250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughes                            NumberFormat.getPercentInstance().format((double) current / total);
225fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey
226fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey                    if (speed > 0) {
227fec5f50a85e1bfc7bb4fa12d04ffa7526c79fad7Jeff Sharkey                        final long remainingMillis = ((total - current) * 1000) / speed;
22852b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkey                        remainingText = res.getString(R.string.download_remaining,
22952b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkey                                DateUtils.formatDuration(remainingMillis));
23052b703c5d0c4cff72bafdec0e2229368d3cc20d0Jeff Sharkey                    }
231a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
232250a1ebc4cf52edf4e55a594f05f4a351ee8e126Elliott Hughes                    final int percent = (int) ((current * 100) / total);
233a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setProgress(100, percent, false);
234a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else {
235a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setProgress(100, 0, true);
236a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
237a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
238a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
239a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Build titles and description
240a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final Notification notif;
241a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (cluster.size() == 1) {
242a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final DownloadInfo info = cluster.iterator().next();
243a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
244a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                builder.setContentTitle(getDownloadTitle(res, info));
245a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
246a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (type == TYPE_ACTIVE) {
247a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    if (!TextUtils.isEmpty(info.mDescription)) {
248a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(info.mDescription);
249a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    } else {
250a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(remainingText);
251a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
252a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentInfo(percentText);
253a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
254a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else if (type == TYPE_WAITING) {
255a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentText(
256a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            res.getString(R.string.notification_need_wifi_for_size));
257a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
258a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else if (type == TYPE_COMPLETE) {
259a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    if (Downloads.Impl.isStatusError(info.mStatus)) {
260a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(res.getText(R.string.notification_download_failed));
261a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    } else if (Downloads.Impl.isStatusSuccess(info.mStatus)) {
262a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                        builder.setContentText(
263a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                                res.getText(R.string.notification_download_complete));
264a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    }
265a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
266a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
267a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                notif = builder.build();
268a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
269a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            } else {
270a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                final Notification.InboxStyle inboxStyle = new Notification.InboxStyle(builder);
271a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
272a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                for (DownloadInfo info : cluster) {
273a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    inboxStyle.addLine(getDownloadTitle(res, info));
274a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
275a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
276a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                if (type == TYPE_ACTIVE) {
277a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentTitle(res.getQuantityString(
278a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            R.plurals.notif_summary_active, cluster.size(), cluster.size()));
279a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentText(remainingText);
280a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentInfo(percentText);
281a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    inboxStyle.setSummaryText(remainingText);
282a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
283a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                } else if (type == TYPE_WAITING) {
284a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentTitle(res.getQuantityString(
285a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            R.plurals.notif_summary_waiting, cluster.size(), cluster.size()));
286a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    builder.setContentText(
287a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            res.getString(R.string.notification_need_wifi_for_size));
288a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                    inboxStyle.setSummaryText(
289a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                            res.getString(R.string.notification_need_wifi_for_size));
290a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                }
291a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
292a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                notif = inboxStyle.build();
293a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
294a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
295a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            mNotifManager.notify(tag, 0, notif);
296a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
297a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
298a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        // Remove stale tags that weren't renewed
299a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        final Iterator<String> it = mActiveNotifs.keySet().iterator();
300a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        while (it.hasNext()) {
301a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            final String tag = it.next();
302a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            if (!clustered.containsKey(tag)) {
303a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                mNotifManager.cancel(tag, 0);
304a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                it.remove();
305a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            }
306a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
307a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
308a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
309a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static CharSequence getDownloadTitle(Resources res, DownloadInfo info) {
310a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        if (!TextUtils.isEmpty(info.mTitle)) {
311a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return info.mTitle;
312a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        } else {
313a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return res.getString(R.string.download_unknown_title);
314a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
315a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
316a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
317a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private long[] getDownloadIds(Collection<DownloadInfo> infos) {
318a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        final long[] ids = new long[infos.size()];
319a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        int i = 0;
320a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        for (DownloadInfo info : infos) {
321a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            ids[i++] = info.mId;
322a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
323a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        return ids;
324a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
325a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
326703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    public void dumpSpeeds() {
327703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey        synchronized (mDownloadSpeed) {
328703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey            for (int i = 0; i < mDownloadSpeed.size(); i++) {
329703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                final long id = mDownloadSpeed.keyAt(i);
330703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                final long delta = SystemClock.elapsedRealtime() - mDownloadTouch.get(id);
331703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                Log.d(TAG, "Download " + id + " speed " + mDownloadSpeed.valueAt(i) + "bps, "
332703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey                        + delta + "ms ago");
333703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey            }
334703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey        }
335703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey    }
336703bc3a83056a878a83e263b992fb5331b84535fJeff Sharkey
337a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
338a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * Build tag used for collapsing several {@link DownloadInfo} into a single
339a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * {@link Notification}.
340a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
341a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static String buildNotificationTag(DownloadInfo info) {
342a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
343a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return TYPE_WAITING + ":" + info.mPackage;
344a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        } else if (isActiveAndVisible(info)) {
345a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return TYPE_ACTIVE + ":" + info.mPackage;
346a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        } else if (isCompleteAndVisible(info)) {
347a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            // Complete downloads always have unique notifs
348a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return TYPE_COMPLETE + ":" + info.mId;
349a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        } else {
350a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey            return null;
351a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        }
352a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
353a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
354a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    /**
355a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * Return the cluster type of the given tag, as created by
356a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     * {@link #buildNotificationTag(DownloadInfo)}.
357a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey     */
358a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static int getNotificationTagType(String tag) {
359a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        return Integer.parseInt(tag.substring(0, tag.indexOf(':')));
360a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
361a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
362a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static boolean isActiveAndVisible(DownloadInfo download) {
3631ad10ce731d1b54692d7d5ee32601e965f503fa4Jeff Sharkey        return download.mStatus == STATUS_RUNNING &&
364a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                (download.mVisibility == VISIBILITY_VISIBLE
365a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
366a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
367a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey
368a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    private static boolean isCompleteAndVisible(DownloadInfo download) {
369a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey        return Downloads.Impl.isStatusCompleted(download.mStatus) &&
370a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                (download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
371a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey                || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
372a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey    }
373a40a349c0107660bfb4004467550725a3ca3ec97Jeff Sharkey}
374