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