• 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 
143             // Use time when cluster was first shown to avoid shuffling
144             final long firstShown;
145             if (mActiveNotifs.containsKey(tag)) {
146                 firstShown = mActiveNotifs.get(tag);
147             } else {
148                 firstShown = System.currentTimeMillis();
149                 mActiveNotifs.put(tag, firstShown);
150             }
151             builder.setWhen(firstShown);
152 
153             // Show relevant icon
154             if (type == TYPE_ACTIVE) {
155                 builder.setSmallIcon(android.R.drawable.stat_sys_download);
156             } else if (type == TYPE_WAITING) {
157                 builder.setSmallIcon(android.R.drawable.stat_sys_warning);
158             } else if (type == TYPE_COMPLETE) {
159                 builder.setSmallIcon(android.R.drawable.stat_sys_download_done);
160             }
161 
162             // Build action intents
163             if (type == TYPE_ACTIVE || type == TYPE_WAITING) {
164                 // build a synthetic uri for intent identification purposes
165                 final Uri uri = new Uri.Builder().scheme("active-dl").appendPath(tag).build();
166                 final Intent intent = new Intent(Constants.ACTION_LIST,
167                         uri, mContext, DownloadReceiver.class);
168                 intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
169                         getDownloadIds(cluster));
170                 builder.setContentIntent(PendingIntent.getBroadcast(mContext,
171                         0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
172                 builder.setOngoing(true);
173 
174             } else if (type == TYPE_COMPLETE) {
175                 final DownloadInfo info = cluster.iterator().next();
176                 final Uri uri = ContentUris.withAppendedId(
177                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, info.mId);
178                 builder.setAutoCancel(true);
179 
180                 final String action;
181                 if (Downloads.Impl.isStatusError(info.mStatus)) {
182                     action = Constants.ACTION_LIST;
183                 } else {
184                     if (info.mDestination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
185                         action = Constants.ACTION_OPEN;
186                     } else {
187                         action = Constants.ACTION_LIST;
188                     }
189                 }
190 
191                 final Intent intent = new Intent(action, uri, mContext, DownloadReceiver.class);
192                 intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
193                         getDownloadIds(cluster));
194                 builder.setContentIntent(PendingIntent.getBroadcast(mContext,
195                         0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
196 
197                 final Intent hideIntent = new Intent(Constants.ACTION_HIDE,
198                         uri, mContext, DownloadReceiver.class);
199                 builder.setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, hideIntent, 0));
200             }
201 
202             // Calculate and show progress
203             String remainingText = null;
204             String percentText = null;
205             if (type == TYPE_ACTIVE) {
206                 long current = 0;
207                 long total = 0;
208                 long speed = 0;
209                 synchronized (mDownloadSpeed) {
210                     for (DownloadInfo info : cluster) {
211                         if (info.mTotalBytes != -1) {
212                             current += info.mCurrentBytes;
213                             total += info.mTotalBytes;
214                             speed += mDownloadSpeed.get(info.mId);
215                         }
216                     }
217                 }
218 
219                 if (total > 0) {
220                     final int percent = (int) ((current * 100) / total);
221                     percentText = res.getString(R.string.download_percent, percent);
222 
223                     if (speed > 0) {
224                         final long remainingMillis = ((total - current) * 1000) / speed;
225                         remainingText = res.getString(R.string.download_remaining,
226                                 DateUtils.formatDuration(remainingMillis));
227                     }
228 
229                     builder.setProgress(100, percent, false);
230                 } else {
231                     builder.setProgress(100, 0, true);
232                 }
233             }
234 
235             // Build titles and description
236             final Notification notif;
237             if (cluster.size() == 1) {
238                 final DownloadInfo info = cluster.iterator().next();
239 
240                 builder.setContentTitle(getDownloadTitle(res, info));
241 
242                 if (type == TYPE_ACTIVE) {
243                     if (!TextUtils.isEmpty(info.mDescription)) {
244                         builder.setContentText(info.mDescription);
245                     } else {
246                         builder.setContentText(remainingText);
247                     }
248                     builder.setContentInfo(percentText);
249 
250                 } else if (type == TYPE_WAITING) {
251                     builder.setContentText(
252                             res.getString(R.string.notification_need_wifi_for_size));
253 
254                 } else if (type == TYPE_COMPLETE) {
255                     if (Downloads.Impl.isStatusError(info.mStatus)) {
256                         builder.setContentText(res.getText(R.string.notification_download_failed));
257                     } else if (Downloads.Impl.isStatusSuccess(info.mStatus)) {
258                         builder.setContentText(
259                                 res.getText(R.string.notification_download_complete));
260                     }
261                 }
262 
263                 notif = builder.build();
264 
265             } else {
266                 final Notification.InboxStyle inboxStyle = new Notification.InboxStyle(builder);
267 
268                 for (DownloadInfo info : cluster) {
269                     inboxStyle.addLine(getDownloadTitle(res, info));
270                 }
271 
272                 if (type == TYPE_ACTIVE) {
273                     builder.setContentTitle(res.getQuantityString(
274                             R.plurals.notif_summary_active, cluster.size(), cluster.size()));
275                     builder.setContentText(remainingText);
276                     builder.setContentInfo(percentText);
277                     inboxStyle.setSummaryText(remainingText);
278 
279                 } else if (type == TYPE_WAITING) {
280                     builder.setContentTitle(res.getQuantityString(
281                             R.plurals.notif_summary_waiting, cluster.size(), cluster.size()));
282                     builder.setContentText(
283                             res.getString(R.string.notification_need_wifi_for_size));
284                     inboxStyle.setSummaryText(
285                             res.getString(R.string.notification_need_wifi_for_size));
286                 }
287 
288                 notif = inboxStyle.build();
289             }
290 
291             mNotifManager.notify(tag, 0, notif);
292         }
293 
294         // Remove stale tags that weren't renewed
295         final Iterator<String> it = mActiveNotifs.keySet().iterator();
296         while (it.hasNext()) {
297             final String tag = it.next();
298             if (!clustered.containsKey(tag)) {
299                 mNotifManager.cancel(tag, 0);
300                 it.remove();
301             }
302         }
303     }
304 
getDownloadTitle(Resources res, DownloadInfo info)305     private static CharSequence getDownloadTitle(Resources res, DownloadInfo info) {
306         if (!TextUtils.isEmpty(info.mTitle)) {
307             return info.mTitle;
308         } else {
309             return res.getString(R.string.download_unknown_title);
310         }
311     }
312 
getDownloadIds(Collection<DownloadInfo> infos)313     private long[] getDownloadIds(Collection<DownloadInfo> infos) {
314         final long[] ids = new long[infos.size()];
315         int i = 0;
316         for (DownloadInfo info : infos) {
317             ids[i++] = info.mId;
318         }
319         return ids;
320     }
321 
dumpSpeeds()322     public void dumpSpeeds() {
323         synchronized (mDownloadSpeed) {
324             for (int i = 0; i < mDownloadSpeed.size(); i++) {
325                 final long id = mDownloadSpeed.keyAt(i);
326                 final long delta = SystemClock.elapsedRealtime() - mDownloadTouch.get(id);
327                 Log.d(TAG, "Download " + id + " speed " + mDownloadSpeed.valueAt(i) + "bps, "
328                         + delta + "ms ago");
329             }
330         }
331     }
332 
333     /**
334      * Build tag used for collapsing several {@link DownloadInfo} into a single
335      * {@link Notification}.
336      */
buildNotificationTag(DownloadInfo info)337     private static String buildNotificationTag(DownloadInfo info) {
338         if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
339             return TYPE_WAITING + ":" + info.mPackage;
340         } else if (isActiveAndVisible(info)) {
341             return TYPE_ACTIVE + ":" + info.mPackage;
342         } else if (isCompleteAndVisible(info)) {
343             // Complete downloads always have unique notifs
344             return TYPE_COMPLETE + ":" + info.mId;
345         } else {
346             return null;
347         }
348     }
349 
350     /**
351      * Return the cluster type of the given tag, as created by
352      * {@link #buildNotificationTag(DownloadInfo)}.
353      */
getNotificationTagType(String tag)354     private static int getNotificationTagType(String tag) {
355         return Integer.parseInt(tag.substring(0, tag.indexOf(':')));
356     }
357 
isActiveAndVisible(DownloadInfo download)358     private static boolean isActiveAndVisible(DownloadInfo download) {
359         return download.mStatus == STATUS_RUNNING &&
360                 (download.mVisibility == VISIBILITY_VISIBLE
361                 || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
362     }
363 
isCompleteAndVisible(DownloadInfo download)364     private static boolean isCompleteAndVisible(DownloadInfo download) {
365         return Downloads.Impl.isStatusCompleted(download.mStatus) &&
366                 (download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_COMPLETED
367                 || download.mVisibility == VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION);
368     }
369 }
370