Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2010 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.email.service;
     18 
     19 import android.accounts.AccountManager;
     20 import android.app.AlarmManager;
     21 import android.app.PendingIntent;
     22 import android.app.Service;
     23 import android.content.BroadcastReceiver;
     24 import android.content.ContentValues;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.database.Cursor;
     28 import android.net.ConnectivityManager;
     29 import android.net.Uri;
     30 import android.os.IBinder;
     31 import android.os.RemoteException;
     32 import android.os.SystemClock;
     33 import android.text.format.DateUtils;
     34 
     35 import com.android.email.AttachmentInfo;
     36 import com.android.email.EmailConnectivityManager;
     37 import com.android.email.NotificationController;
     38 import com.android.email2.ui.MailActivityEmail;
     39 import com.android.emailcommon.provider.Account;
     40 import com.android.emailcommon.provider.EmailContent;
     41 import com.android.emailcommon.provider.EmailContent.Attachment;
     42 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     43 import com.android.emailcommon.provider.EmailContent.Message;
     44 import com.android.emailcommon.service.EmailServiceProxy;
     45 import com.android.emailcommon.service.EmailServiceStatus;
     46 import com.android.emailcommon.service.IEmailServiceCallback;
     47 import com.android.emailcommon.utility.AttachmentUtilities;
     48 import com.android.emailcommon.utility.Utility;
     49 import com.android.mail.providers.UIProvider.AttachmentState;
     50 import com.android.mail.utils.LogUtils;
     51 
     52 import java.io.File;
     53 import java.io.FileDescriptor;
     54 import java.io.PrintWriter;
     55 import java.util.Comparator;
     56 import java.util.HashMap;
     57 import java.util.Iterator;
     58 import java.util.TreeSet;
     59 import java.util.concurrent.ConcurrentHashMap;
     60 
     61 public class AttachmentDownloadService extends Service implements Runnable {
     62     public static final String TAG = LogUtils.TAG;
     63 
     64     // Minimum wait time before retrying a download that failed due to connection error
     65     private static final long CONNECTION_ERROR_RETRY_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS;
     66     // Number of retries before we start delaying between
     67     private static final long CONNECTION_ERROR_DELAY_THRESHOLD = 5;
     68     // Maximum time to retry for connection errors.
     69     private static final long CONNECTION_ERROR_MAX_RETRIES = 10;
     70 
     71     // Our idle time, waiting for notifications; this is something of a failsafe
     72     private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS);
     73     // How often our watchdog checks for callback timeouts
     74     private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS);
     75     // How long we'll wait for a callback before canceling a download and retrying
     76     private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS);
     77     // Try to download an attachment in the background this many times before giving up
     78     private static final int MAX_DOWNLOAD_RETRIES = 5;
     79     private static final int PRIORITY_NONE = -1;
     80     @SuppressWarnings("unused")
     81     // Low priority will be used for opportunistic downloads
     82     private static final int PRIORITY_BACKGROUND = 0;
     83     // Normal priority is for forwarded downloads in outgoing mail
     84     private static final int PRIORITY_SEND_MAIL = 1;
     85     // High priority is for user requests
     86     private static final int PRIORITY_FOREGROUND = 2;
     87 
     88     // Minimum free storage in order to perform prefetch (25% of total memory)
     89     private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F;
     90     // Maximum prefetch storage (also 25% of total memory)
     91     private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F;
     92 
     93     // We can try various values here; I think 2 is completely reasonable as a first pass
     94     private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
     95     // Limit on the number of simultaneous downloads per account
     96     // Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
     97     private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
     98     // Limit on the number of attachments we'll check for background download
     99     private static final int MAX_ATTACHMENTS_TO_CHECK = 25;
    100 
    101     private static final String EXTRA_ATTACHMENT =
    102         "com.android.email.AttachmentDownloadService.attachment";
    103 
    104     // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
    105     // by the use of "volatile"
    106     /*package*/ static volatile AttachmentDownloadService sRunningService = null;
    107 
    108     /*package*/ Context mContext;
    109     /*package*/ EmailConnectivityManager mConnectivityManager;
    110 
    111     /*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator());
    112 
    113     private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
    114     // A map of attachment storage used per account
    115     // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated
    116     // amount plus the size of any new attachments laoded).  If and when we reach the per-account
    117     // limit, we recalculate the actual usage
    118     /*package*/ final HashMap<Long, Long> mAttachmentStorageMap = new HashMap<Long, Long>();
    119     // A map of attachment ids to the number of failed attempts to download the attachment
    120     // NOTE: We do not want to persist this. This allows us to retry background downloading
    121     // if any transient network errors are fixed & and the app is restarted
    122     /* package */ final HashMap<Long, Integer> mAttachmentFailureMap = new HashMap<Long, Integer>();
    123     private final ServiceCallback mServiceCallback = new ServiceCallback();
    124 
    125     private final Object mLock = new Object();
    126     private volatile boolean mStop = false;
    127 
    128     /*package*/ AccountManagerStub mAccountManagerStub;
    129 
    130     /**
    131      * We only use the getAccounts() call from AccountManager, so this class wraps that call and
    132      * allows us to build a mock account manager stub in the unit tests
    133      */
    134     /*package*/ static class AccountManagerStub {
    135         private int mNumberOfAccounts;
    136         private final AccountManager mAccountManager;
    137 
    138         AccountManagerStub(Context context) {
    139             if (context != null) {
    140                 mAccountManager = AccountManager.get(context);
    141             } else {
    142                 mAccountManager = null;
    143             }
    144         }
    145 
    146         /*package*/ int getNumberOfAccounts() {
    147             if (mAccountManager != null) {
    148                 return mAccountManager.getAccounts().length;
    149             } else {
    150                 return mNumberOfAccounts;
    151             }
    152         }
    153 
    154         /*package*/ void setNumberOfAccounts(int numberOfAccounts) {
    155             mNumberOfAccounts = numberOfAccounts;
    156         }
    157     }
    158 
    159     /**
    160      * Watchdog alarm receiver; responsible for making sure that downloads in progress are not
    161      * stalled, as determined by the timing of the most recent service callback
    162      */
    163     public static class Watchdog extends BroadcastReceiver {
    164         @Override
    165         public void onReceive(final Context context, Intent intent) {
    166             new Thread(new Runnable() {
    167                 @Override
    168                 public void run() {
    169                     watchdogAlarm();
    170                 }
    171             }, "AttachmentDownloadService Watchdog").start();
    172         }
    173     }
    174 
    175     public static class DownloadRequest {
    176         final int priority;
    177         final long time;
    178         final long attachmentId;
    179         final long messageId;
    180         final long accountId;
    181         boolean inProgress = false;
    182         int lastStatusCode;
    183         int lastProgress;
    184         long lastCallbackTime;
    185         long startTime;
    186         long retryCount;
    187         long retryStartTime;
    188 
    189         private DownloadRequest(Context context, Attachment attachment) {
    190             attachmentId = attachment.mId;
    191             Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
    192             if (msg != null) {
    193                 accountId = msg.mAccountKey;
    194                 messageId = msg.mId;
    195             } else {
    196                 accountId = messageId = -1;
    197             }
    198             priority = getPriority(attachment);
    199             time = SystemClock.elapsedRealtime();
    200         }
    201 
    202         private DownloadRequest(DownloadRequest orig, long newTime) {
    203             priority = orig.priority;
    204             attachmentId = orig.attachmentId;
    205             messageId = orig.messageId;
    206             accountId = orig.accountId;
    207             time = newTime;
    208             inProgress = orig.inProgress;
    209             lastStatusCode = orig.lastStatusCode;
    210             lastProgress = orig.lastProgress;
    211             lastCallbackTime = orig.lastCallbackTime;
    212             startTime = orig.startTime;
    213             retryCount = orig.retryCount;
    214             retryStartTime = orig.retryStartTime;
    215         }
    216 
    217 
    218         @Override
    219         public int hashCode() {
    220             return (int)attachmentId;
    221         }
    222 
    223         /**
    224          * Two download requests are equals if their attachment id's are equals
    225          */
    226         @Override
    227         public boolean equals(Object object) {
    228             if (!(object instanceof DownloadRequest)) return false;
    229             DownloadRequest req = (DownloadRequest)object;
    230             return req.attachmentId == attachmentId;
    231         }
    232     }
    233 
    234     /**
    235      * Comparator class for the download set; we first compare by priority.  Requests with equal
    236      * priority are compared by the time the request was created (older requests come first)
    237      */
    238     /*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> {
    239         @Override
    240         public int compare(DownloadRequest req1, DownloadRequest req2) {
    241             int res;
    242             if (req1.priority != req2.priority) {
    243                 res = (req1.priority < req2.priority) ? -1 : 1;
    244             } else {
    245                 if (req1.time == req2.time) {
    246                     res = 0;
    247                 } else {
    248                     res = (req1.time > req2.time) ? -1 : 1;
    249                 }
    250             }
    251             return res;
    252         }
    253     }
    254 
    255     /**
    256      * The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the
    257      * time of the request.  Higher priority requests
    258      * are always processed first; among equals, the oldest request is processed first.  The
    259      * priority key represents this ordering.  Note: All methods that change the attachment map are
    260      * synchronized on the map itself
    261      */
    262     /*package*/ class DownloadSet extends TreeSet<DownloadRequest> {
    263         private static final long serialVersionUID = 1L;
    264         private PendingIntent mWatchdogPendingIntent;
    265 
    266         /*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) {
    267             super(comparator);
    268         }
    269 
    270         /**
    271          * Maps attachment id to DownloadRequest
    272          */
    273         /*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
    274             new ConcurrentHashMap<Long, DownloadRequest>();
    275 
    276         /**
    277          * onChange is called by the AttachmentReceiver upon receipt of a valid notification from
    278          * EmailProvider that an attachment has been inserted or modified.  It's not strictly
    279          * necessary that we detect a deleted attachment, as the code always checks for the
    280          * existence of an attachment before acting on it.
    281          */
    282         public synchronized void onChange(Context context, Attachment att) {
    283             DownloadRequest req = findDownloadRequest(att.mId);
    284             long priority = getPriority(att);
    285             if (priority == PRIORITY_NONE) {
    286                 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    287                     LogUtils.d(TAG, "== Attachment changed: " + att.mId);
    288                 }
    289                 // In this case, there is no download priority for this attachment
    290                 if (req != null) {
    291                     // If it exists in the map, remove it
    292                     // NOTE: We don't yet support deleting downloads in progress
    293                     if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    294                         LogUtils.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
    295                     }
    296                     remove(req);
    297                 }
    298             } else {
    299                 // Ignore changes that occur during download
    300                 if (mDownloadsInProgress.containsKey(att.mId)) return;
    301                 // If this is new, add the request to the queue
    302                 if (req == null) {
    303                     req = new DownloadRequest(context, att);
    304                     add(req);
    305                 }
    306                 // If the request already existed, we'll update the priority (so that the time is
    307                 // up-to-date); otherwise, we create a new request
    308                 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    309                     LogUtils.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
    310                             req.priority + ", priority time " + req.time);
    311                 }
    312             }
    313             // Process the queue if we're in a wait
    314             kick();
    315         }
    316 
    317         /**
    318          * Find a queued DownloadRequest, given the attachment's id
    319          * @param id the id of the attachment
    320          * @return the DownloadRequest for that attachment (or null, if none)
    321          */
    322         /*package*/ synchronized DownloadRequest findDownloadRequest(long id) {
    323             Iterator<DownloadRequest> iterator = iterator();
    324             while(iterator.hasNext()) {
    325                 DownloadRequest req = iterator.next();
    326                 if (req.attachmentId == id) {
    327                     return req;
    328                 }
    329             }
    330             return null;
    331         }
    332 
    333         @Override
    334         public synchronized boolean isEmpty() {
    335             return super.isEmpty() && mDownloadsInProgress.isEmpty();
    336         }
    337 
    338         /**
    339          * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
    340          * the limit on maximum downloads
    341          */
    342         /*package*/ synchronized void processQueue() {
    343             if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    344                 LogUtils.d(TAG, "== Checking attachment queue, " + mDownloadSet.size()
    345                         + " entries");
    346             }
    347 
    348             Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
    349             // First, start up any required downloads, in priority order
    350             while (iterator.hasNext() &&
    351                     (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
    352                 DownloadRequest req = iterator.next();
    353                  // Enforce per-account limit here
    354                 if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
    355                     if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    356                         LogUtils.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
    357                                 req.accountId);
    358                     }
    359                     continue;
    360                 } else if (Attachment.restoreAttachmentWithId(mContext, req.attachmentId) == null) {
    361                     continue;
    362                 }
    363                 if (!req.inProgress) {
    364                     final long currentTime = SystemClock.elapsedRealtime();
    365                     if (req.retryCount > 0 && req.retryStartTime > currentTime) {
    366                         LogUtils.d(TAG, "== waiting to retry attachment %d", req.attachmentId);
    367                         setWatchdogAlarm(CONNECTION_ERROR_RETRY_MILLIS);
    368                         continue;
    369                     }
    370                     mDownloadSet.tryStartDownload(req);
    371                 }
    372             }
    373 
    374             // Don't prefetch if background downloading is disallowed
    375             EmailConnectivityManager ecm = mConnectivityManager;
    376             if (ecm == null) return;
    377             if (!ecm.isAutoSyncAllowed()) return;
    378             // Don't prefetch unless we're on a WiFi network
    379             if (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI) {
    380                 return;
    381             }
    382             // Then, try opportunistic download of appropriate attachments
    383             int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
    384             // Always leave one slot for user requested download
    385             if (backgroundDownloads > (MAX_SIMULTANEOUS_DOWNLOADS - 1)) {
    386                 // We'll load up the newest 25 attachments that aren't loaded or queued
    387                 Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
    388                         MAX_ATTACHMENTS_TO_CHECK);
    389                 Cursor c = mContext.getContentResolver().query(lookupUri,
    390                         Attachment.CONTENT_PROJECTION,
    391                         EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
    392                         null, Attachment.RECORD_ID + " DESC");
    393                 File cacheDir = mContext.getCacheDir();
    394                 try {
    395                     while (c.moveToNext()) {
    396                         Attachment att = new Attachment();
    397                         att.restore(c);
    398                         Account account = Account.restoreAccountWithId(mContext, att.mAccountKey);
    399                         if (account == null) {
    400                             // Clean up this orphaned attachment; there's no point in keeping it
    401                             // around; then try to find another one
    402                             EmailContent.delete(mContext, Attachment.CONTENT_URI, att.mId);
    403                         } else {
    404                             // Check that the attachment meets system requirements for download
    405                             AttachmentInfo info = new AttachmentInfo(mContext, att);
    406                             if (info.isEligibleForDownload()) {
    407                                 // Either the account must be able to prefetch or this must be
    408                                 // an inline attachment
    409                                 if (att.mContentId != null ||
    410                                         (canPrefetchForAccount(account, cacheDir))) {
    411                                     Integer tryCount;
    412                                     tryCount = mAttachmentFailureMap.get(att.mId);
    413                                     if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) {
    414                                         // move onto the next attachment
    415                                         continue;
    416                                     }
    417                                     // Start this download and we're done
    418                                     DownloadRequest req = new DownloadRequest(mContext, att);
    419                                     mDownloadSet.tryStartDownload(req);
    420                                     break;
    421                                 }
    422                             }
    423                         }
    424                     }
    425                 } finally {
    426                     c.close();
    427                 }
    428             }
    429         }
    430 
    431         /**
    432          * Count the number of running downloads in progress for this account
    433          * @param accountId the id of the account
    434          * @return the count of running downloads
    435          */
    436         /*package*/ synchronized int downloadsForAccount(long accountId) {
    437             int count = 0;
    438             for (DownloadRequest req: mDownloadsInProgress.values()) {
    439                 if (req.accountId == accountId) {
    440                     count++;
    441                 }
    442             }
    443             return count;
    444         }
    445 
    446         /**
    447          * Watchdog for downloads; we use this in case we are hanging on a download, which might
    448          * have failed silently (the connection dropped, for example)
    449          */
    450         private void onWatchdogAlarm() {
    451             // If our service instance is gone, just leave
    452             if (mStop) {
    453                 return;
    454             }
    455             long now = System.currentTimeMillis();
    456             for (DownloadRequest req: mDownloadsInProgress.values()) {
    457                 // Check how long it's been since receiving a callback
    458                 long timeSinceCallback = now - req.lastCallbackTime;
    459                 if (timeSinceCallback > CALLBACK_TIMEOUT) {
    460                     if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    461                         LogUtils.d(TAG, "== Download of " + req.attachmentId + " timed out");
    462                     }
    463                     cancelDownload(req);
    464                 }
    465             }
    466             // Check whether we can start new downloads...
    467             if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) {
    468                 processQueue();
    469             }
    470             // If there are downloads in progress, reset alarm
    471             if (!mDownloadsInProgress.isEmpty()) {
    472                 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    473                     LogUtils.d(TAG, "Reschedule watchdog...");
    474                 }
    475                 setWatchdogAlarm();
    476             }
    477         }
    478 
    479         /**
    480          * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
    481          * parameter
    482          * @param req the DownloadRequest
    483          * @return whether or not the download was started
    484          */
    485         /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
    486             EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
    487                     AttachmentDownloadService.this, req.accountId);
    488 
    489             // Do not download the same attachment multiple times
    490             boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null;
    491             if (alreadyInProgress) return false;
    492 
    493             try {
    494                 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    495                     LogUtils.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
    496                 }
    497                 startDownload(service, req);
    498             } catch (RemoteException e) {
    499                 // TODO: Consider whether we need to do more in this case...
    500                 // For now, fix up our data to reflect the failure
    501                 cancelDownload(req);
    502             }
    503             return true;
    504         }
    505 
    506         private synchronized DownloadRequest getDownloadInProgress(long attachmentId) {
    507             return mDownloadsInProgress.get(attachmentId);
    508         }
    509 
    510         private void setWatchdogAlarm(final long delay) {
    511             // Lazily initialize the pending intent
    512             if (mWatchdogPendingIntent == null) {
    513                 Intent intent = new Intent(mContext, Watchdog.class);
    514                 mWatchdogPendingIntent =
    515                     PendingIntent.getBroadcast(mContext, 0, intent, 0);
    516             }
    517             // Set the alarm
    518             AlarmManager am = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
    519             am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay,
    520                     mWatchdogPendingIntent);
    521         }
    522 
    523         private void setWatchdogAlarm() {
    524             setWatchdogAlarm(WATCHDOG_CHECK_INTERVAL);
    525         }
    526 
    527         /**
    528          * Do the work of starting an attachment download using the EmailService interface, and
    529          * set our watchdog alarm
    530          *
    531          * @param service the service handling the download
    532          * @param req the DownloadRequest
    533          * @throws RemoteException
    534          */
    535         private void startDownload(EmailServiceProxy service, DownloadRequest req)
    536                 throws RemoteException {
    537             req.startTime = System.currentTimeMillis();
    538             req.inProgress = true;
    539             mDownloadsInProgress.put(req.attachmentId, req);
    540             service.loadAttachment(mServiceCallback, req.attachmentId,
    541                     req.priority != PRIORITY_FOREGROUND);
    542             setWatchdogAlarm();
    543         }
    544 
    545         private void cancelDownload(DownloadRequest req) {
    546             LogUtils.d(TAG, "cancelDownload #%d", req.attachmentId);
    547             req.inProgress = false;
    548             mDownloadsInProgress.remove(req.attachmentId);
    549             // Remove the download from our queue, and then decide whether or not to add it back.
    550             remove(req);
    551             req.retryCount++;
    552             if (req.retryCount > CONNECTION_ERROR_MAX_RETRIES) {
    553                 LogUtils.d(TAG, "too many failures, giving up");
    554             } else {
    555                 LogUtils.d(TAG, "moving to end of queue, will retry");
    556                 // The time field of DownloadRequest is final, because it's unsafe to change it
    557                 // as long as the DownloadRequest is in the DownloadSet. It's needed for the
    558                 // comparator, so changing time would make the request unfindable.
    559                 // Instead, we'll create a new DownloadRequest with an updated time.
    560                 // This will sort at the end of the set.
    561                 req = new DownloadRequest(req, SystemClock.elapsedRealtime());
    562                 add(req);
    563             }
    564         }
    565 
    566         /**
    567          * Called when a download is finished; we get notified of this via our EmailServiceCallback
    568          * @param attachmentId the id of the attachment whose download is finished
    569          * @param statusCode the EmailServiceStatus code returned by the Service
    570          */
    571         /*package*/ synchronized void endDownload(long attachmentId, int statusCode) {
    572             // Say we're no longer downloading this
    573             mDownloadsInProgress.remove(attachmentId);
    574 
    575             // TODO: This code is conservative and treats connection issues as failures.
    576             // Since we have no mechanism to throttle reconnection attempts, it makes
    577             // sense to be cautious here. Once logic is in place to prevent connecting
    578             // in a tight loop, we can exclude counting connection issues as "failures".
    579 
    580             // Update the attachment failure list if needed
    581             Integer downloadCount;
    582             downloadCount = mAttachmentFailureMap.remove(attachmentId);
    583             if (statusCode != EmailServiceStatus.SUCCESS) {
    584                 if (downloadCount == null) {
    585                     downloadCount = 0;
    586                 }
    587                 downloadCount += 1;
    588                 mAttachmentFailureMap.put(attachmentId, downloadCount);
    589             }
    590 
    591             DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
    592             if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
    593                 // If this needs to be retried, just process the queue again
    594                 if (req != null) {
    595                     req.retryCount++;
    596                     if (req.retryCount > CONNECTION_ERROR_MAX_RETRIES) {
    597                         LogUtils.d(TAG, "Connection Error #%d, giving up", attachmentId);
    598                         remove(req);
    599                     } else if (req.retryCount > CONNECTION_ERROR_DELAY_THRESHOLD) {
    600                         // TODO: I'm not sure this is a great retry/backoff policy, but we're
    601                         // afraid of changing behavior too much in case something relies upon it.
    602                         // So now, for the first five errors, we'll retry immediately. For the next
    603                         // five tries, we'll add a ten second delay between each. After that, we'll
    604                         // give up.
    605                         LogUtils.d(TAG, "ConnectionError #%d, retried %d times, adding delay",
    606                                 attachmentId, req.retryCount);
    607                         req.inProgress = false;
    608                         req.retryStartTime = SystemClock.elapsedRealtime() +
    609                                 CONNECTION_ERROR_RETRY_MILLIS;
    610                         setWatchdogAlarm(CONNECTION_ERROR_RETRY_MILLIS);
    611                     } else {
    612                         LogUtils.d(TAG, "ConnectionError #%d, retried %d times, adding delay",
    613                                 attachmentId, req.retryCount);
    614                         req.inProgress = false;
    615                         req.retryStartTime = 0;
    616                         kick();
    617                     }
    618                 }
    619                 return;
    620             }
    621 
    622             // If the request is still in the queue, remove it
    623             if (req != null) {
    624                 remove(req);
    625             }
    626             if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    627                 long secs = 0;
    628                 if (req != null) {
    629                     secs = (System.currentTimeMillis() - req.time) / 1000;
    630                 }
    631                 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
    632                     "Error " + statusCode;
    633                 LogUtils.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs
    634                         + " seconds from request, status: " + status);
    635             }
    636 
    637             Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
    638             if (attachment != null) {
    639                 long accountId = attachment.mAccountKey;
    640                 // Update our attachment storage for this account
    641                 Long currentStorage = mAttachmentStorageMap.get(accountId);
    642                 if (currentStorage == null) {
    643                     currentStorage = 0L;
    644                 }
    645                 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize);
    646                 boolean deleted = false;
    647                 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
    648                     if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
    649                         // If this is a forwarding download, and the attachment doesn't exist (or
    650                         // can't be downloaded) delete it from the outgoing message, lest that
    651                         // message never get sent
    652                         EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
    653                         // TODO: Talk to UX about whether this is even worth doing
    654                         NotificationController nc = NotificationController.getInstance(mContext);
    655                         nc.showDownloadForwardFailedNotification(attachment);
    656                         deleted = true;
    657                     }
    658                     // If we're an attachment on forwarded mail, and if we're not still blocked,
    659                     // try to send pending mail now (as mediated by MailService)
    660                     if ((req != null) &&
    661                             !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
    662                         if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    663                             LogUtils.d(TAG, "== Downloads finished for outgoing msg #"
    664                                     + req.messageId);
    665                         }
    666                         EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
    667                                 mContext, accountId);
    668                         try {
    669                             service.sendMail(accountId);
    670                         } catch (RemoteException e) {
    671                             // We tried
    672                         }
    673                     }
    674                 }
    675                 if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
    676                     Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
    677                     if (msg == null) {
    678                         // If there's no associated message, delete the attachment
    679                         EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
    680                     } else {
    681                         // If there really is a message, retry
    682                         // TODO: How will this get retried? It's still marked as inProgress?
    683                         kick();
    684                         return;
    685                     }
    686                 } else if (!deleted) {
    687                     // Clear the download flags, since we're done for now.  Note that this happens
    688                     // only for non-recoverable errors.  When these occur for forwarded mail, we can
    689                     // ignore it and continue; otherwise, it was either 1) a user request, in which
    690                     // case the user can retry manually or 2) an opportunistic download, in which
    691                     // case the download wasn't critical
    692                     ContentValues cv = new ContentValues();
    693                     int flags =
    694                         Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    695                     cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags);
    696                     cv.put(Attachment.UI_STATE, AttachmentState.SAVED);
    697                     attachment.update(mContext, cv);
    698                 }
    699             }
    700             // Process the queue
    701             kick();
    702         }
    703     }
    704 
    705     /**
    706      * Calculate the download priority of an Attachment.  A priority of zero means that the
    707      * attachment is not marked for download.
    708      * @param att the Attachment
    709      * @return the priority key of the Attachment
    710      */
    711     private static int getPriority(Attachment att) {
    712         int priorityClass = PRIORITY_NONE;
    713         int flags = att.mFlags;
    714         if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
    715             priorityClass = PRIORITY_SEND_MAIL;
    716         } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
    717             priorityClass = PRIORITY_FOREGROUND;
    718         }
    719         return priorityClass;
    720     }
    721 
    722     private void kick() {
    723         synchronized(mLock) {
    724             mLock.notify();
    725         }
    726     }
    727 
    728     /**
    729      * We use an EmailServiceCallback to keep track of the progress of downloads.  These callbacks
    730      * come from either Controller (IMAP) or ExchangeService (EAS).  Note that we only implement the
    731      * single callback that's defined by the EmailServiceCallback interface.
    732      */
    733     private class ServiceCallback extends IEmailServiceCallback.Stub {
    734         @Override
    735         public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
    736                 int progress) {
    737             // Record status and progress
    738             DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId);
    739             if (req != null) {
    740                 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    741                     String code;
    742                     switch(statusCode) {
    743                         case EmailServiceStatus.SUCCESS: code = "Success"; break;
    744                         case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break;
    745                         default: code = Integer.toString(statusCode); break;
    746                     }
    747                     if (statusCode != EmailServiceStatus.IN_PROGRESS) {
    748                         LogUtils.d(TAG, ">> Attachment status " + attachmentId + ": " + code);
    749                     } else if (progress >= (req.lastProgress + 10)) {
    750                         LogUtils.d(TAG, ">> Attachment progress %d: %d%%", attachmentId, progress);
    751                     }
    752                 }
    753                 req.lastStatusCode = statusCode;
    754                 req.lastProgress = progress;
    755                 req.lastCallbackTime = System.currentTimeMillis();
    756                 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
    757                  if (attachment != null  && statusCode == EmailServiceStatus.IN_PROGRESS) {
    758                     ContentValues values = new ContentValues();
    759                     values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
    760                             attachment.mSize * progress / 100);
    761                     // Update UIProvider with updated download size
    762                     // Individual services will set contentUri and state when finished
    763                     attachment.update(mContext, values);
    764                 }
    765             }
    766             switch (statusCode) {
    767                 case EmailServiceStatus.IN_PROGRESS:
    768                     break;
    769                 default:
    770                     mDownloadSet.endDownload(attachmentId, statusCode);
    771                     break;
    772             }
    773         }
    774     }
    775 
    776     /*package*/ void addServiceIntentForTest(long accountId, Intent intent) {
    777         mAccountServiceMap.put(accountId, intent);
    778     }
    779 
    780     /*package*/ void onChange(Attachment att) {
    781         mDownloadSet.onChange(this, att);
    782     }
    783 
    784     /*package*/ boolean isQueued(long attachmentId) {
    785         return mDownloadSet.findDownloadRequest(attachmentId) != null;
    786     }
    787 
    788     /*package*/ int getSize() {
    789         return mDownloadSet.size();
    790     }
    791 
    792     /*package*/ boolean dequeue(long attachmentId) {
    793         DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
    794         if (req != null) {
    795             if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    796                 LogUtils.d(TAG, "Dequeued attachmentId:  " + attachmentId);
    797             }
    798             mDownloadSet.remove(req);
    799             return true;
    800         }
    801         return false;
    802     }
    803 
    804     /**
    805      * Ask the service for the number of items in the download queue
    806      * @return the number of items queued for download
    807      */
    808     public static int getQueueSize() {
    809         AttachmentDownloadService service = sRunningService;
    810         if (service != null) {
    811             return service.getSize();
    812         }
    813         return 0;
    814     }
    815 
    816     /**
    817      * Ask the service whether a particular attachment is queued for download
    818      * @param attachmentId the id of the Attachment (as stored by EmailProvider)
    819      * @return whether or not the attachment is queued for download
    820      */
    821     public static boolean isAttachmentQueued(long attachmentId) {
    822         AttachmentDownloadService service = sRunningService;
    823         if (service != null) {
    824             return service.isQueued(attachmentId);
    825         }
    826         return false;
    827     }
    828 
    829     /**
    830      * Ask the service to remove an attachment from the download queue
    831      * @param attachmentId the id of the Attachment (as stored by EmailProvider)
    832      * @return whether or not the attachment was removed from the queue
    833      */
    834     public static boolean cancelQueuedAttachment(long attachmentId) {
    835         AttachmentDownloadService service = sRunningService;
    836         if (service != null) {
    837             return service.dequeue(attachmentId);
    838         }
    839         return false;
    840     }
    841 
    842     public static void watchdogAlarm() {
    843         AttachmentDownloadService service = sRunningService;
    844         if (service != null) {
    845             service.mDownloadSet.onWatchdogAlarm();
    846         }
    847     }
    848 
    849     /**
    850      * Called directly by EmailProvider whenever an attachment is inserted or changed
    851      * @param context the caller's context
    852      * @param id the attachment's id
    853      * @param flags the new flags for the attachment
    854      */
    855     public static void attachmentChanged(final Context context, final long id, final int flags) {
    856         Utility.runAsync(new Runnable() {
    857             @Override
    858             public void run() {
    859                 Attachment attachment = Attachment.restoreAttachmentWithId(context, id);
    860                 if (attachment != null) {
    861                     // Store the flags we got from EmailProvider; given that all of this
    862                     // activity is asynchronous, we need to use the newest data from
    863                     // EmailProvider
    864                     attachment.mFlags = flags;
    865                     Intent intent = new Intent(context, AttachmentDownloadService.class);
    866                     intent.putExtra(EXTRA_ATTACHMENT, attachment);
    867                     context.startService(intent);
    868                 }
    869             }});
    870     }
    871 
    872     /**
    873      * Determine whether an attachment can be prefetched for the given account
    874      * @return true if download is allowed, false otherwise
    875      */
    876     public boolean canPrefetchForAccount(Account account, File dir) {
    877         // Check account, just in case
    878         if (account == null) return false;
    879         // First, check preference and quickly return if prefetch isn't allowed
    880         if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false;
    881 
    882         long totalStorage = dir.getTotalSpace();
    883         long usableStorage = dir.getUsableSpace();
    884         long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
    885 
    886         // If there's not enough overall storage available, stop now
    887         if (usableStorage < minAvailable) {
    888             return false;
    889         }
    890 
    891         int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
    892         long perAccountMaxStorage =
    893             (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
    894 
    895         // Retrieve our idea of currently used attachment storage; since we don't track deletions,
    896         // this number is the "worst case".  If the number is greater than what's allowed per
    897         // account, we walk the directory to determine the actual number
    898         Long accountStorage = mAttachmentStorageMap.get(account.mId);
    899         if (accountStorage == null || (accountStorage > perAccountMaxStorage)) {
    900             // Calculate the exact figure for attachment storage for this account
    901             accountStorage = 0L;
    902             File[] files = dir.listFiles();
    903             if (files != null) {
    904                 for (File file : files) {
    905                     accountStorage += file.length();
    906                 }
    907             }
    908             // Cache the value
    909             mAttachmentStorageMap.put(account.mId, accountStorage);
    910         }
    911 
    912         // Return true if we're using less than the maximum per account
    913         if (accountStorage < perAccountMaxStorage) {
    914             return true;
    915         } else {
    916             if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
    917                 LogUtils.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " +
    918                         accountStorage + ", limit " + perAccountMaxStorage);
    919             }
    920             return false;
    921         }
    922     }
    923 
    924     @Override
    925     public void run() {
    926         // These fields are only used within the service thread
    927         mContext = this;
    928         mConnectivityManager = new EmailConnectivityManager(this, TAG);
    929         mAccountManagerStub = new AccountManagerStub(this);
    930 
    931         // Run through all attachments in the database that require download and add them to
    932         // the queue
    933         int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    934         Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
    935                 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0",
    936                 new String[] {Integer.toString(mask)}, null);
    937         try {
    938             LogUtils.d(TAG, "Count: " + c.getCount());
    939             while (c.moveToNext()) {
    940                 Attachment attachment = Attachment.restoreAttachmentWithId(
    941                         this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
    942                 if (attachment != null) {
    943                     mDownloadSet.onChange(this, attachment);
    944                 }
    945             }
    946         } catch (Exception e) {
    947             e.printStackTrace();
    948         }
    949         finally {
    950             c.close();
    951         }
    952 
    953         // Loop until stopped, with a 30 minute wait loop
    954         while (!mStop) {
    955             // Here's where we run our attachment loading logic...
    956             // Make a local copy of the variable so we don't null-crash on service shutdown
    957             final EmailConnectivityManager ecm = mConnectivityManager;
    958             if (ecm != null) {
    959                 ecm.waitForConnectivity();
    960             }
    961             if (mStop) {
    962                 // We might be bailing out here due to the service shutting down
    963                 break;
    964             }
    965             mDownloadSet.processQueue();
    966             if (mDownloadSet.isEmpty()) {
    967                 LogUtils.d(TAG, "*** All done; shutting down service");
    968                 stopSelf();
    969                 break;
    970             }
    971             synchronized(mLock) {
    972                 try {
    973                     mLock.wait(PROCESS_QUEUE_WAIT_TIME);
    974                 } catch (InterruptedException e) {
    975                     // That's ok; we'll just keep looping
    976                 }
    977             }
    978         }
    979 
    980         // Unregister now that we're done
    981         // Make a local copy of the variable so we don't null-crash on service shutdown
    982         final EmailConnectivityManager ecm = mConnectivityManager;
    983         if (ecm != null) {
    984             ecm.unregister();
    985         }
    986     }
    987 
    988     @Override
    989     public int onStartCommand(Intent intent, int flags, int startId) {
    990         if (sRunningService == null) {
    991             sRunningService = this;
    992         }
    993         if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) {
    994             Attachment att = (Attachment)intent.getParcelableExtra(EXTRA_ATTACHMENT);
    995             onChange(att);
    996         }
    997         return Service.START_STICKY;
    998     }
    999 
   1000     @Override
   1001     public void onCreate() {
   1002         // Start up our service thread
   1003         new Thread(this, "AttachmentDownloadService").start();
   1004     }
   1005     @Override
   1006     public IBinder onBind(Intent intent) {
   1007         return null;
   1008     }
   1009 
   1010     @Override
   1011     public void onDestroy() {
   1012         // Mark this instance of the service as stopped
   1013         mStop = true;
   1014         if (sRunningService != null) {
   1015             kick();
   1016             sRunningService = null;
   1017         }
   1018         if (mConnectivityManager != null) {
   1019             mConnectivityManager.unregister();
   1020             mConnectivityManager.stopWait();
   1021             mConnectivityManager = null;
   1022         }
   1023     }
   1024 
   1025     @Override
   1026     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
   1027         pw.println("AttachmentDownloadService");
   1028         long time = System.currentTimeMillis();
   1029         synchronized(mDownloadSet) {
   1030             pw.println("  Queue, " + mDownloadSet.size() + " entries");
   1031             Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
   1032             // First, start up any required downloads, in priority order
   1033             while (iterator.hasNext()) {
   1034                 DownloadRequest req = iterator.next();
   1035                 pw.println("    Account: " + req.accountId + ", Attachment: " + req.attachmentId);
   1036                 pw.println("      Priority: " + req.priority + ", Time: " + req.time +
   1037                         (req.inProgress ? " [In progress]" : ""));
   1038                 Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId);
   1039                 if (att == null) {
   1040                     pw.println("      Attachment not in database?");
   1041                 } else if (att.mFileName != null) {
   1042                     String fileName = att.mFileName;
   1043                     String suffix = "[none]";
   1044                     int lastDot = fileName.lastIndexOf('.');
   1045                     if (lastDot >= 0) {
   1046                         suffix = fileName.substring(lastDot);
   1047                     }
   1048                     pw.print("      Suffix: " + suffix);
   1049                     if (att.getContentUri() != null) {
   1050                         pw.print(" ContentUri: " + att.getContentUri());
   1051                     }
   1052                     pw.print(" Mime: ");
   1053                     if (att.mMimeType != null) {
   1054                         pw.print(att.mMimeType);
   1055                     } else {
   1056                         pw.print(AttachmentUtilities.inferMimeType(fileName, null));
   1057                         pw.print(" [inferred]");
   1058                     }
   1059                     pw.println(" Size: " + att.mSize);
   1060                 }
   1061                 if (req.inProgress) {
   1062                     pw.println("      Status: " + req.lastStatusCode + ", Progress: " +
   1063                             req.lastProgress);
   1064                     pw.println("      Started: " + req.startTime + ", Callback: " +
   1065                             req.lastCallbackTime);
   1066                     pw.println("      Elapsed: " + ((time - req.startTime) / 1000L) + "s");
   1067                     if (req.lastCallbackTime > 0) {
   1068                         pw.println("      CB: " + ((time - req.lastCallbackTime) / 1000L) + "s");
   1069                     }
   1070                 }
   1071             }
   1072         }
   1073     }
   1074 }
   1075