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 17package com.android.providers.downloads; 18 19import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE; 20import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED; 21import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION; 22import static android.provider.Downloads.Impl.STATUS_RUNNING; 23import static com.android.providers.downloads.Constants.TAG; 24 25import android.app.DownloadManager; 26import android.app.Notification; 27import android.app.NotificationManager; 28import android.app.PendingIntent; 29import android.content.ContentUris; 30import android.content.Context; 31import android.content.Intent; 32import android.content.res.Resources; 33import android.net.Uri; 34import android.os.SystemClock; 35import android.provider.Downloads; 36import android.text.TextUtils; 37import android.text.format.DateUtils; 38import android.util.Log; 39import android.util.LongSparseLongArray; 40 41import com.google.common.collect.ArrayListMultimap; 42import com.google.common.collect.Maps; 43import com.google.common.collect.Multimap; 44 45import java.util.Collection; 46import java.util.HashMap; 47import java.util.Iterator; 48 49import 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 */ 56public 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 // 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 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 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 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 */ 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 */ 354 private static int getNotificationTagType(String tag) { 355 return Integer.parseInt(tag.substring(0, tag.indexOf(':'))); 356 } 357 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 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