Home | History | Annotate | Download | only in downloads
      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                 // 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