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