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