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