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 = 15 * ((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                 public void run() {
    159                     watchdogAlarm();
    160                 }
    161             }, "AttachmentDownloadService Watchdog").start();
    162         }
    163     }
    164 
    165     public static class DownloadRequest {
    166         final int priority;
    167         final long time;
    168         final long attachmentId;
    169         final long messageId;
    170         final long accountId;
    171         boolean inProgress = false;
    172         int lastStatusCode;
    173         int lastProgress;
    174         long lastCallbackTime;
    175         long startTime;
    176 
    177         private DownloadRequest(Context context, Attachment attachment) {
    178             attachmentId = attachment.mId;
    179             Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
    180             if (msg != null) {
    181                 accountId = msg.mAccountKey;
    182                 messageId = msg.mId;
    183             } else {
    184                 accountId = messageId = -1;
    185             }
    186             priority = getPriority(attachment);
    187             time = System.currentTimeMillis();
    188         }
    189 
    190         @Override
    191         public int hashCode() {
    192             return (int)attachmentId;
    193         }
    194 
    195         /**
    196          * Two download requests are equals if their attachment id's are equals
    197          */
    198         @Override
    199         public boolean equals(Object object) {
    200             if (!(object instanceof DownloadRequest)) return false;
    201             DownloadRequest req = (DownloadRequest)object;
    202             return req.attachmentId == attachmentId;
    203         }
    204     }
    205 
    206     /**
    207      * Comparator class for the download set; we first compare by priority.  Requests with equal
    208      * priority are compared by the time the request was created (older requests come first)
    209      */
    210     /*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> {
    211         @Override
    212         public int compare(DownloadRequest req1, DownloadRequest req2) {
    213             int res;
    214             if (req1.priority != req2.priority) {
    215                 res = (req1.priority < req2.priority) ? -1 : 1;
    216             } else {
    217                 if (req1.time == req2.time) {
    218                     res = 0;
    219                 } else {
    220                     res = (req1.time > req2.time) ? -1 : 1;
    221                 }
    222             }
    223             return res;
    224         }
    225     }
    226 
    227     /**
    228      * The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the
    229      * time of the request.  Higher priority requests
    230      * are always processed first; among equals, the oldest request is processed first.  The
    231      * priority key represents this ordering.  Note: All methods that change the attachment map are
    232      * synchronized on the map itself
    233      */
    234     /*package*/ class DownloadSet extends TreeSet<DownloadRequest> {
    235         private static final long serialVersionUID = 1L;
    236         private PendingIntent mWatchdogPendingIntent;
    237         private AlarmManager mAlarmManager;
    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) return;
    415             long now = System.currentTimeMillis();
    416             for (DownloadRequest req: mDownloadsInProgress.values()) {
    417                 // Check how long it's been since receiving a callback
    418                 long timeSinceCallback = now - req.lastCallbackTime;
    419                 if (timeSinceCallback > CALLBACK_TIMEOUT) {
    420                     if (Email.DEBUG) {
    421                         Log.d(TAG, "== Download of " + req.attachmentId + " timed out");
    422                     }
    423                    cancelDownload(req);
    424                 }
    425             }
    426             // If there are downloads in progress, reset alarm
    427             if (mDownloadsInProgress.isEmpty()) {
    428                 if (mAlarmManager != null && mWatchdogPendingIntent != null) {
    429                     mAlarmManager.cancel(mWatchdogPendingIntent);
    430                 }
    431             }
    432             // Check whether we can start new downloads...
    433             if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) {
    434                 processQueue();
    435             }
    436         }
    437 
    438         /**
    439          * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
    440          * parameter
    441          * @param req the DownloadRequest
    442          * @return whether or not the download was started
    443          */
    444         /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
    445             Intent intent = getServiceIntentForAccount(req.accountId);
    446             if (intent == null) return false;
    447 
    448             // Do not download the same attachment multiple times
    449             boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null;
    450             if (alreadyInProgress) return false;
    451 
    452             try {
    453                 if (Email.DEBUG) {
    454                     Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
    455                 }
    456                 startDownload(intent, req);
    457             } catch (RemoteException e) {
    458                 // TODO: Consider whether we need to do more in this case...
    459                 // For now, fix up our data to reflect the failure
    460                 cancelDownload(req);
    461             }
    462             return true;
    463         }
    464 
    465         private synchronized DownloadRequest getDownloadInProgress(long attachmentId) {
    466             return mDownloadsInProgress.get(attachmentId);
    467         }
    468 
    469         /**
    470          * Do the work of starting an attachment download using the EmailService interface, and
    471          * set our watchdog alarm
    472          *
    473          * @param serviceClass the class that will attempt the download
    474          * @param req the DownloadRequest
    475          * @throws RemoteException
    476          */
    477         private void startDownload(Intent intent, DownloadRequest req)
    478                 throws RemoteException {
    479             req.startTime = System.currentTimeMillis();
    480             req.inProgress = true;
    481             mDownloadsInProgress.put(req.attachmentId, req);
    482             EmailServiceProxy proxy =
    483                 new EmailServiceProxy(mContext, intent, mServiceCallback);
    484             proxy.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND);
    485             // Lazily initialize our (reusable) pending intent
    486             if (mWatchdogPendingIntent == null) {
    487                 createWatchdogPendingIntent(mContext);
    488             }
    489             // Set the alarm
    490             mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
    491                     System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, WATCHDOG_CHECK_INTERVAL,
    492                     mWatchdogPendingIntent);
    493         }
    494 
    495         /*package*/ void createWatchdogPendingIntent(Context context) {
    496             Intent alarmIntent = new Intent(context, Watchdog.class);
    497             mWatchdogPendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0);
    498             mAlarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    499         }
    500 
    501         private void cancelDownload(DownloadRequest req) {
    502             mDownloadsInProgress.remove(req.attachmentId);
    503             req.inProgress = false;
    504         }
    505 
    506         /**
    507          * Called when a download is finished; we get notified of this via our EmailServiceCallback
    508          * @param attachmentId the id of the attachment whose download is finished
    509          * @param statusCode the EmailServiceStatus code returned by the Service
    510          */
    511         /*package*/ synchronized void endDownload(long attachmentId, int statusCode) {
    512             // Say we're no longer downloading this
    513             mDownloadsInProgress.remove(attachmentId);
    514 
    515             // TODO: This code is conservative and treats connection issues as failures.
    516             // Since we have no mechanism to throttle reconnection attempts, it makes
    517             // sense to be cautious here. Once logic is in place to prevent connecting
    518             // in a tight loop, we can exclude counting connection issues as "failures".
    519 
    520             // Update the attachment failure list if needed
    521             Integer downloadCount;
    522             downloadCount = mAttachmentFailureMap.remove(attachmentId);
    523             if (statusCode != EmailServiceStatus.SUCCESS) {
    524                 if (downloadCount == null) {
    525                     downloadCount = 0;
    526                 }
    527                 downloadCount += 1;
    528                 mAttachmentFailureMap.put(attachmentId, downloadCount);
    529             }
    530 
    531             DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
    532             if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
    533                 // If this needs to be retried, just process the queue again
    534                 if (Email.DEBUG) {
    535                     Log.d(TAG, "== The download for attachment #" + attachmentId +
    536                             " will be retried");
    537                 }
    538                 if (req != null) {
    539                     req.inProgress = false;
    540                 }
    541                 kick();
    542                 return;
    543             }
    544 
    545             // If the request is still in the queue, remove it
    546             if (req != null) {
    547                 remove(req);
    548             }
    549             if (Email.DEBUG) {
    550                 long secs = 0;
    551                 if (req != null) {
    552                     secs = (System.currentTimeMillis() - req.time) / 1000;
    553                 }
    554                 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
    555                     "Error " + statusCode;
    556                 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs +
    557                            " seconds from request, status: " + status);
    558             }
    559 
    560             Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
    561             if (attachment != null) {
    562                 long accountId = attachment.mAccountKey;
    563                 // Update our attachment storage for this account
    564                 Long currentStorage = mAttachmentStorageMap.get(accountId);
    565                 if (currentStorage == null) {
    566                     currentStorage = 0L;
    567                 }
    568                 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize);
    569                 boolean deleted = false;
    570                 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
    571                     if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
    572                         // If this is a forwarding download, and the attachment doesn't exist (or
    573                         // can't be downloaded) delete it from the outgoing message, lest that
    574                         // message never get sent
    575                         EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
    576                         // TODO: Talk to UX about whether this is even worth doing
    577                         NotificationController nc = NotificationController.getInstance(mContext);
    578                         nc.showDownloadForwardFailedNotification(attachment);
    579                         deleted = true;
    580                     }
    581                     // If we're an attachment on forwarded mail, and if we're not still blocked,
    582                     // try to send pending mail now (as mediated by MailService)
    583                     if ((req != null) &&
    584                             !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
    585                         if (Email.DEBUG) {
    586                             Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId);
    587                         }
    588                         MailService.actionSendPendingMail(mContext, req.accountId);
    589                     }
    590                 }
    591                 if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
    592                     Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
    593                     if (msg == null) {
    594                         // If there's no associated message, delete the attachment
    595                         EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
    596                     } else {
    597                         // If there really is a message, retry
    598                         kick();
    599                         return;
    600                     }
    601                 } else if (!deleted) {
    602                     // Clear the download flags, since we're done for now.  Note that this happens
    603                     // only for non-recoverable errors.  When these occur for forwarded mail, we can
    604                     // ignore it and continue; otherwise, it was either 1) a user request, in which
    605                     // case the user can retry manually or 2) an opportunistic download, in which
    606                     // case the download wasn't critical
    607                     ContentValues cv = new ContentValues();
    608                     int flags =
    609                         Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    610                     cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags);
    611                     attachment.update(mContext, cv);
    612                 }
    613             }
    614             // Process the queue
    615             kick();
    616         }
    617     }
    618 
    619     /**
    620      * Calculate the download priority of an Attachment.  A priority of zero means that the
    621      * attachment is not marked for download.
    622      * @param att the Attachment
    623      * @return the priority key of the Attachment
    624      */
    625     private static int getPriority(Attachment att) {
    626         int priorityClass = PRIORITY_NONE;
    627         int flags = att.mFlags;
    628         if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
    629             priorityClass = PRIORITY_SEND_MAIL;
    630         } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
    631             priorityClass = PRIORITY_FOREGROUND;
    632         }
    633         return priorityClass;
    634     }
    635 
    636     private void kick() {
    637         synchronized(mLock) {
    638             mLock.notify();
    639         }
    640     }
    641 
    642     /**
    643      * We use an EmailServiceCallback to keep track of the progress of downloads.  These callbacks
    644      * come from either Controller (IMAP) or ExchangeService (EAS).  Note that we only implement the
    645      * single callback that's defined by the EmailServiceCallback interface.
    646      */
    647     private class ServiceCallback extends IEmailServiceCallback.Stub {
    648         public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
    649                 int progress) {
    650             // Record status and progress
    651             DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId);
    652             if (req != null) {
    653                 if (Email.DEBUG) {
    654                     String code;
    655                     switch(statusCode) {
    656                         case EmailServiceStatus.SUCCESS: code = "Success"; break;
    657                         case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break;
    658                         default: code = Integer.toString(statusCode); break;
    659                     }
    660                     if (statusCode != EmailServiceStatus.IN_PROGRESS) {
    661                         Log.d(TAG, ">> Attachment " + attachmentId + ": " + code);
    662                     } else if (progress >= (req.lastProgress + 15)) {
    663                         Log.d(TAG, ">> Attachment " + attachmentId + ": " + progress + "%");
    664                     }
    665                 }
    666                 req.lastStatusCode = statusCode;
    667                 req.lastProgress = progress;
    668                 req.lastCallbackTime = System.currentTimeMillis();
    669             }
    670             switch (statusCode) {
    671                 case EmailServiceStatus.IN_PROGRESS:
    672                     break;
    673                 default:
    674                     mDownloadSet.endDownload(attachmentId, statusCode);
    675                     break;
    676             }
    677         }
    678 
    679         @Override
    680         public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
    681                 throws RemoteException {
    682         }
    683 
    684         @Override
    685         public void syncMailboxListStatus(long accountId, int statusCode, int progress)
    686                 throws RemoteException {
    687         }
    688 
    689         @Override
    690         public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
    691                 throws RemoteException {
    692         }
    693     }
    694 
    695     /**
    696      * Return an Intent to be used used based on the account type of the provided account id.  We
    697      * cache the results to avoid repeated database access
    698      * @param accountId the id of the account
    699      * @return the Intent to be used for the account or null (if the account no longer exists)
    700      */
    701     private synchronized Intent getServiceIntentForAccount(long accountId) {
    702         // TODO: We should have some more data-driven way of determining the service intent.
    703         Intent serviceIntent = mAccountServiceMap.get(accountId);
    704         if (serviceIntent == null) {
    705             String protocol = Account.getProtocol(mContext, accountId);
    706             if (protocol == null) return null;
    707             serviceIntent = new Intent(mContext, ControllerService.class);
    708             if (protocol.equals("eas")) {
    709                 serviceIntent = new Intent(EmailServiceProxy.EXCHANGE_INTENT);
    710             }
    711             mAccountServiceMap.put(accountId, serviceIntent);
    712         }
    713         return serviceIntent;
    714     }
    715 
    716     /*package*/ void addServiceIntentForTest(long accountId, Intent intent) {
    717         mAccountServiceMap.put(accountId, intent);
    718     }
    719 
    720     /*package*/ void onChange(Attachment att) {
    721         mDownloadSet.onChange(this, att);
    722     }
    723 
    724     /*package*/ boolean isQueued(long attachmentId) {
    725         return mDownloadSet.findDownloadRequest(attachmentId) != null;
    726     }
    727 
    728     /*package*/ int getSize() {
    729         return mDownloadSet.size();
    730     }
    731 
    732     /*package*/ boolean dequeue(long attachmentId) {
    733         DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
    734         if (req != null) {
    735             if (Email.DEBUG) {
    736                 Log.d(TAG, "Dequeued attachmentId:  " + attachmentId);
    737             }
    738             mDownloadSet.remove(req);
    739             return true;
    740         }
    741         return false;
    742     }
    743 
    744     /**
    745      * Ask the service for the number of items in the download queue
    746      * @return the number of items queued for download
    747      */
    748     public static int getQueueSize() {
    749         AttachmentDownloadService service = sRunningService;
    750         if (service != null) {
    751             return service.getSize();
    752         }
    753         return 0;
    754     }
    755 
    756     /**
    757      * Ask the service whether a particular attachment is queued for download
    758      * @param attachmentId the id of the Attachment (as stored by EmailProvider)
    759      * @return whether or not the attachment is queued for download
    760      */
    761     public static boolean isAttachmentQueued(long attachmentId) {
    762         AttachmentDownloadService service = sRunningService;
    763         if (service != null) {
    764             return service.isQueued(attachmentId);
    765         }
    766         return false;
    767     }
    768 
    769     /**
    770      * Ask the service to remove an attachment from the download queue
    771      * @param attachmentId the id of the Attachment (as stored by EmailProvider)
    772      * @return whether or not the attachment was removed from the queue
    773      */
    774     public static boolean cancelQueuedAttachment(long attachmentId) {
    775         AttachmentDownloadService service = sRunningService;
    776         if (service != null) {
    777             return service.dequeue(attachmentId);
    778         }
    779         return false;
    780     }
    781 
    782     public static void watchdogAlarm() {
    783         AttachmentDownloadService service = sRunningService;
    784         if (service != null) {
    785             service.mDownloadSet.onWatchdogAlarm();
    786         }
    787     }
    788 
    789     /**
    790      * Called directly by EmailProvider whenever an attachment is inserted or changed
    791      * @param context the caller's context
    792      * @param id the attachment's id
    793      * @param flags the new flags for the attachment
    794      */
    795     public static void attachmentChanged(final Context context, final long id, final int flags) {
    796         Utility.runAsync(new Runnable() {
    797             public void run() {
    798                 Attachment attachment = Attachment.restoreAttachmentWithId(context, id);
    799                 if (attachment != null) {
    800                     // Store the flags we got from EmailProvider; given that all of this
    801                     // activity is asynchronous, we need to use the newest data from
    802                     // EmailProvider
    803                     attachment.mFlags = flags;
    804                     Intent intent = new Intent(context, AttachmentDownloadService.class);
    805                     intent.putExtra(EXTRA_ATTACHMENT, attachment);
    806                     context.startService(intent);
    807                 }
    808             }});
    809     }
    810 
    811     /**
    812      * Determine whether an attachment can be prefetched for the given account
    813      * @return true if download is allowed, false otherwise
    814      */
    815     public boolean canPrefetchForAccount(Account account, File dir) {
    816         // Check account, just in case
    817         if (account == null) return false;
    818         // First, check preference and quickly return if prefetch isn't allowed
    819         if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false;
    820 
    821         long totalStorage = dir.getTotalSpace();
    822         long usableStorage = dir.getUsableSpace();
    823         long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
    824 
    825         // If there's not enough overall storage available, stop now
    826         if (usableStorage < minAvailable) {
    827             return false;
    828         }
    829 
    830         int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
    831         long perAccountMaxStorage =
    832             (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
    833 
    834         // Retrieve our idea of currently used attachment storage; since we don't track deletions,
    835         // this number is the "worst case".  If the number is greater than what's allowed per
    836         // account, we walk the directory to determine the actual number
    837         Long accountStorage = mAttachmentStorageMap.get(account.mId);
    838         if (accountStorage == null || (accountStorage > perAccountMaxStorage)) {
    839             // Calculate the exact figure for attachment storage for this account
    840             accountStorage = 0L;
    841             File[] files = dir.listFiles();
    842             if (files != null) {
    843                 for (File file : files) {
    844                     accountStorage += file.length();
    845                 }
    846             }
    847             // Cache the value
    848             mAttachmentStorageMap.put(account.mId, accountStorage);
    849         }
    850 
    851         // Return true if we're using less than the maximum per account
    852         if (accountStorage < perAccountMaxStorage) {
    853             return true;
    854         } else {
    855             if (Email.DEBUG) {
    856                 Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " +
    857                         accountStorage + ", limit " + perAccountMaxStorage);
    858             }
    859             return false;
    860         }
    861     }
    862 
    863     public void run() {
    864         // These fields are only used within the service thread
    865         mContext = this;
    866         mConnectivityManager = new EmailConnectivityManager(this, TAG);
    867         mAccountManagerStub = new AccountManagerStub(this);
    868 
    869         // Run through all attachments in the database that require download and add them to
    870         // the queue
    871         int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    872         Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
    873                 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0",
    874                 new String[] {Integer.toString(mask)}, null);
    875         try {
    876             Log.d(TAG, "Count: " + c.getCount());
    877             while (c.moveToNext()) {
    878                 Attachment attachment = Attachment.restoreAttachmentWithId(
    879                         this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
    880                 if (attachment != null) {
    881                     mDownloadSet.onChange(this, attachment);
    882                 }
    883             }
    884         } catch (Exception e) {
    885             e.printStackTrace();
    886         }
    887         finally {
    888             c.close();
    889         }
    890 
    891         // Loop until stopped, with a 30 minute wait loop
    892         while (!mStop) {
    893             // Here's where we run our attachment loading logic...
    894             mConnectivityManager.waitForConnectivity();
    895             mDownloadSet.processQueue();
    896             if (mDownloadSet.isEmpty()) {
    897                 Log.d(TAG, "*** All done; shutting down service");
    898                 stopSelf();
    899                 break;
    900             }
    901             synchronized(mLock) {
    902                 try {
    903                     mLock.wait(PROCESS_QUEUE_WAIT_TIME);
    904                 } catch (InterruptedException e) {
    905                     // That's ok; we'll just keep looping
    906                 }
    907             }
    908         }
    909 
    910         // Unregister now that we're done
    911         if (mConnectivityManager != null) {
    912             mConnectivityManager.unregister();
    913         }
    914     }
    915 
    916     @Override
    917     public int onStartCommand(Intent intent, int flags, int startId) {
    918         if (sRunningService == null) {
    919             sRunningService = this;
    920         }
    921         if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) {
    922             Attachment att = (Attachment)intent.getParcelableExtra(EXTRA_ATTACHMENT);
    923             onChange(att);
    924         }
    925         return Service.START_STICKY;
    926     }
    927 
    928     @Override
    929     public void onCreate() {
    930         // Start up our service thread
    931         new Thread(this, "AttachmentDownloadService").start();
    932     }
    933     @Override
    934     public IBinder onBind(Intent intent) {
    935         return null;
    936     }
    937 
    938     @Override
    939     public void onDestroy() {
    940         // Mark this instance of the service as stopped
    941         mStop = true;
    942         if (sRunningService != null) {
    943             kick();
    944             sRunningService = null;
    945         }
    946         if (mConnectivityManager != null) {
    947             mConnectivityManager.unregister();
    948             mConnectivityManager = null;
    949         }
    950     }
    951 
    952     @Override
    953     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    954         pw.println("AttachmentDownloadService");
    955         long time = System.currentTimeMillis();
    956         synchronized(mDownloadSet) {
    957             pw.println("  Queue, " + mDownloadSet.size() + " entries");
    958             Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
    959             // First, start up any required downloads, in priority order
    960             while (iterator.hasNext()) {
    961                 DownloadRequest req = iterator.next();
    962                 pw.println("    Account: " + req.accountId + ", Attachment: " + req.attachmentId);
    963                 pw.println("      Priority: " + req.priority + ", Time: " + req.time +
    964                         (req.inProgress ? " [In progress]" : ""));
    965                 Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId);
    966                 if (att == null) {
    967                     pw.println("      Attachment not in database?");
    968                 } else if (att.mFileName != null) {
    969                     String fileName = att.mFileName;
    970                     String suffix = "[none]";
    971                     int lastDot = fileName.lastIndexOf('.');
    972                     if (lastDot >= 0) {
    973                         suffix = fileName.substring(lastDot);
    974                     }
    975                     pw.print("      Suffix: " + suffix);
    976                     if (att.mContentUri != null) {
    977                         pw.print(" ContentUri: " + att.mContentUri);
    978                     }
    979                     pw.print(" Mime: ");
    980                     if (att.mMimeType != null) {
    981                         pw.print(att.mMimeType);
    982                     } else {
    983                         pw.print(AttachmentUtilities.inferMimeType(fileName, null));
    984                         pw.print(" [inferred]");
    985                     }
    986                     pw.println(" Size: " + att.mSize);
    987                 }
    988                 if (req.inProgress) {
    989                     pw.println("      Status: " + req.lastStatusCode + ", Progress: " +
    990                             req.lastProgress);
    991                     pw.println("      Started: " + req.startTime + ", Callback: " +
    992                             req.lastCallbackTime);
    993                     pw.println("      Elapsed: " + ((time - req.startTime) / 1000L) + "s");
    994                     if (req.lastCallbackTime > 0) {
    995                         pw.println("      CB: " + ((time - req.lastCallbackTime) / 1000L) + "s");
    996                     }
    997                 }
    998             }
    999         }
   1000     }
   1001 }
   1002