Home | History | Annotate | Download | only in service
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.service;
     18 
     19 import android.accounts.AccountManager;
     20 import android.app.AlarmManager;
     21 import android.app.PendingIntent;
     22 import android.app.Service;
     23 import android.content.BroadcastReceiver;
     24 import android.content.ContentValues;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.database.Cursor;
     28 import android.net.ConnectivityManager;
     29 import android.net.Uri;
     30 import android.os.IBinder;
     31 import android.os.RemoteException;
     32 import android.os.SystemClock;
     33 import android.text.format.DateUtils;
     34 
     35 import com.android.email.AttachmentInfo;
     36 import com.android.email.EmailConnectivityManager;
     37 import com.android.email.NotificationController;
     38 import com.android.emailcommon.provider.Account;
     39 import com.android.emailcommon.provider.EmailContent;
     40 import com.android.emailcommon.provider.EmailContent.Attachment;
     41 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     42 import com.android.emailcommon.provider.EmailContent.Message;
     43 import com.android.emailcommon.service.EmailServiceProxy;
     44 import com.android.emailcommon.service.EmailServiceStatus;
     45 import com.android.emailcommon.service.IEmailServiceCallback;
     46 import com.android.emailcommon.utility.AttachmentUtilities;
     47 import com.android.emailcommon.utility.Utility;
     48 import com.android.mail.providers.UIProvider.AttachmentState;
     49 import com.android.mail.utils.LogUtils;
     50 import com.google.common.annotations.VisibleForTesting;
     51 
     52 import java.io.File;
     53 import java.io.FileDescriptor;
     54 import java.io.PrintWriter;
     55 import java.util.Collection;
     56 import java.util.Comparator;
     57 import java.util.HashMap;
     58 import java.util.PriorityQueue;
     59 import java.util.Queue;
     60 import java.util.concurrent.ConcurrentHashMap;
     61 import java.util.concurrent.ConcurrentLinkedQueue;
     62 
     63 public class AttachmentService extends Service implements Runnable {
     64     // For logging.
     65     public static final String LOG_TAG = "AttachmentService";
     66 
     67     // STOPSHIP Set this to 0 before shipping.
     68     private static final int ENABLE_ATTACHMENT_SERVICE_DEBUG = 0;
     69 
     70     // Minimum wait time before retrying a download that failed due to connection error
     71     private static final long CONNECTION_ERROR_RETRY_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS;
     72     // Number of retries before we start delaying between
     73     private static final long CONNECTION_ERROR_DELAY_THRESHOLD = 5;
     74     // Maximum time to retry for connection errors.
     75     private static final long CONNECTION_ERROR_MAX_RETRIES = 10;
     76 
     77     // Our idle time, waiting for notifications; this is something of a failsafe
     78     private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS);
     79     // How long we'll wait for a callback before canceling a download and retrying
     80     private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS);
     81     // Try to download an attachment in the background this many times before giving up
     82     private static final int MAX_DOWNLOAD_RETRIES = 5;
     83 
     84     static final int PRIORITY_NONE = -1;
     85     // High priority is for user requests
     86     static final int PRIORITY_FOREGROUND = 0;
     87     static final int PRIORITY_HIGHEST = PRIORITY_FOREGROUND;
     88     // Normal priority is for forwarded downloads in outgoing mail
     89     static final int PRIORITY_SEND_MAIL = 1;
     90     // Low priority will be used for opportunistic downloads
     91     static final int PRIORITY_BACKGROUND = 2;
     92     static final int PRIORITY_LOWEST = PRIORITY_BACKGROUND;
     93 
     94     // Minimum free storage in order to perform prefetch (25% of total memory)
     95     private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F;
     96     // Maximum prefetch storage (also 25% of total memory)
     97     private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F;
     98 
     99     // We can try various values here; I think 2 is completely reasonable as a first pass
    100     private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
    101     // Limit on the number of simultaneous downloads per account
    102     // Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
    103     private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
    104     // Limit on the number of attachments we'll check for background download
    105     private static final int MAX_ATTACHMENTS_TO_CHECK = 25;
    106 
    107     private static final String EXTRA_ATTACHMENT_ID =
    108             "com.android.email.AttachmentService.attachment_id";
    109     private static final String EXTRA_ATTACHMENT_FLAGS =
    110             "com.android.email.AttachmentService.attachment_flags";
    111 
    112     // This callback is invoked by the various service implementations to give us download progress
    113     // since those modules are responsible for the actual download.
    114     final ServiceCallback mServiceCallback = new ServiceCallback();
    115 
    116     // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
    117     // by the use of "volatile"
    118     static volatile AttachmentService sRunningService = null;
    119 
    120     // Signify that we are being shut down & destroyed.
    121     private volatile boolean mStop = false;
    122 
    123     EmailConnectivityManager mConnectivityManager;
    124 
    125     // Helper class that keeps track of in progress downloads to make sure that they
    126     // are progressing well.
    127     final AttachmentWatchdog mWatchdog = new AttachmentWatchdog();
    128 
    129     private final Object mLock = new Object();
    130 
    131     // A map of attachment storage used per account as we have account based maximums to follow.
    132     // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated
    133     // amount plus the size of any new attachments loaded).  If and when we reach the per-account
    134     // limit, we recalculate the actual usage
    135     final ConcurrentHashMap<Long, Long> mAttachmentStorageMap = new ConcurrentHashMap<Long, Long>();
    136 
    137     // A map of attachment ids to the number of failed attempts to download the attachment
    138     // NOTE: We do not want to persist this. This allows us to retry background downloading
    139     // if any transient network errors are fixed & and the app is restarted
    140     final ConcurrentHashMap<Long, Integer> mAttachmentFailureMap = new ConcurrentHashMap<Long, Integer>();
    141 
    142     // Keeps tracks of downloads in progress based on an attachment ID to DownloadRequest mapping.
    143     final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
    144             new ConcurrentHashMap<Long, DownloadRequest>();
    145 
    146     final DownloadQueue mDownloadQueue = new DownloadQueue();
    147 
    148     // The queue entries here are entries of the form {id, flags}, with the values passed in to
    149     // attachmentChanged(). Entries in the queue are picked off in processQueue().
    150     private static final Queue<long[]> sAttachmentChangedQueue =
    151             new ConcurrentLinkedQueue<long[]>();
    152 
    153     // Extra layer of control over debug logging that should only be enabled when
    154     // we need to take an extra deep dive at debugging the workflow in this class.
    155     static private void debugTrace(final String format, final Object... args) {
    156         if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) {
    157             LogUtils.d(LOG_TAG, String.format(format, args));
    158         }
    159     }
    160 
    161     /**
    162      * This class is used to contain the details and state of a particular request to download
    163      * an attachment. These objects are constructed and either placed in the {@link DownloadQueue}
    164      * or in the in-progress map used to keep track of downloads that are currently happening
    165      * in the system
    166      */
    167     static class DownloadRequest {
    168         // Details of the request.
    169         final int mPriority;
    170         final long mCreatedTime;
    171         final long mAttachmentId;
    172         final long mMessageId;
    173         final long mAccountId;
    174 
    175         // Status of the request.
    176         boolean mInProgress = false;
    177         int mLastStatusCode;
    178         int mLastProgress;
    179         long mLastCallbackTime;
    180         long mStartTime;
    181         long mRetryCount;
    182         long mRetryStartTime;
    183 
    184         /**
    185          * This constructor is mainly used for tests
    186          * @param attPriority The priority of this attachment
    187          * @param attId The id of the row in the attachment table.
    188          */
    189         @VisibleForTesting
    190         DownloadRequest(final int attPriority, final long attId) {
    191             // This constructor should only be used for unit tests.
    192             mCreatedTime = SystemClock.elapsedRealtime();
    193             mPriority = attPriority;
    194             mAttachmentId = attId;
    195             mAccountId = -1;
    196             mMessageId = -1;
    197         }
    198 
    199         private DownloadRequest(final Context context, final Attachment attachment) {
    200             mAttachmentId = attachment.mId;
    201             final Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
    202             if (msg != null) {
    203                 mAccountId = msg.mAccountKey;
    204                 mMessageId = msg.mId;
    205             } else {
    206                 mAccountId = mMessageId = -1;
    207             }
    208             mPriority = getAttachmentPriority(attachment);
    209             mCreatedTime = SystemClock.elapsedRealtime();
    210         }
    211 
    212         private DownloadRequest(final DownloadRequest orig, final long newTime) {
    213             mPriority = orig.mPriority;
    214             mAttachmentId = orig.mAttachmentId;
    215             mMessageId = orig.mMessageId;
    216             mAccountId = orig.mAccountId;
    217             mCreatedTime = newTime;
    218             mInProgress = orig.mInProgress;
    219             mLastStatusCode = orig.mLastStatusCode;
    220             mLastProgress = orig.mLastProgress;
    221             mLastCallbackTime = orig.mLastCallbackTime;
    222             mStartTime = orig.mStartTime;
    223             mRetryCount = orig.mRetryCount;
    224             mRetryStartTime = orig.mRetryStartTime;
    225         }
    226 
    227         @Override
    228         public int hashCode() {
    229             return (int)mAttachmentId;
    230         }
    231 
    232         /**
    233          * Two download requests are equals if their attachment id's are equals
    234          */
    235         @Override
    236         public boolean equals(final Object object) {
    237             if (!(object instanceof DownloadRequest)) return false;
    238             final DownloadRequest req = (DownloadRequest)object;
    239             return req.mAttachmentId == mAttachmentId;
    240         }
    241     }
    242 
    243     /**
    244      * This class is used to organize the various download requests that are pending.
    245      * We need a class that allows us to prioritize a collection of {@link DownloadRequest} objects
    246      * while being able to pull off request with the highest priority but we also need
    247      * to be able to find a particular {@link DownloadRequest} by id or by reference for retrieval.
    248      * Bonus points for an implementation that does not require an iterator to accomplish its tasks
    249      * as we can avoid pesky ConcurrentModificationException when one thread has the iterator
    250      * and another thread modifies the collection.
    251      */
    252     static class DownloadQueue {
    253         private final int DEFAULT_SIZE = 10;
    254 
    255         // For synchronization
    256         private final Object mLock = new Object();
    257 
    258         /**
    259          * Comparator class for the download set; we first compare by priority.  Requests with equal
    260          * priority are compared by the time the request was created (older requests come first)
    261          */
    262         private static class DownloadComparator implements Comparator<DownloadRequest> {
    263             @Override
    264             public int compare(DownloadRequest req1, DownloadRequest req2) {
    265                 int res;
    266                 if (req1.mPriority != req2.mPriority) {
    267                     res = (req1.mPriority < req2.mPriority) ? -1 : 1;
    268                 } else {
    269                     if (req1.mCreatedTime == req2.mCreatedTime) {
    270                         res = 0;
    271                     } else {
    272                         res = (req1.mCreatedTime < req2.mCreatedTime) ? -1 : 1;
    273                     }
    274                 }
    275                 return res;
    276             }
    277         }
    278 
    279         // For prioritization of DownloadRequests.
    280         final PriorityQueue<DownloadRequest> mRequestQueue =
    281                 new PriorityQueue<DownloadRequest>(DEFAULT_SIZE, new DownloadComparator());
    282 
    283         // Secondary collection to quickly find objects w/o the help of an iterator.
    284         // This class should be kept in lock step with the priority queue.
    285         final ConcurrentHashMap<Long, DownloadRequest> mRequestMap =
    286                 new ConcurrentHashMap<Long, DownloadRequest>();
    287 
    288         /**
    289          * This function will add the request to our collections if it does not already
    290          * exist. If it does exist, the function will silently succeed.
    291          * @param request The {@link DownloadRequest} that should be added to our queue
    292          * @return true if it was added (or already exists), false otherwise
    293          */
    294         public boolean addRequest(final DownloadRequest request)
    295                 throws NullPointerException {
    296             // It is key to keep the map and queue in lock step
    297             if (request == null) {
    298                 // We can't add a null entry into the queue so let's throw what the underlying
    299                 // data structure would throw.
    300                 throw new NullPointerException();
    301             }
    302             final long requestId = request.mAttachmentId;
    303             if (requestId < 0) {
    304                 // Invalid request
    305                 LogUtils.d(LOG_TAG, "Not adding a DownloadRequest with an invalid attachment id");
    306                 return false;
    307             }
    308             debugTrace("Queuing DownloadRequest #%d", requestId);
    309             synchronized (mLock) {
    310                 // Check to see if this request is is already in the queue
    311                 final boolean exists = mRequestMap.containsKey(requestId);
    312                 if (!exists) {
    313                     mRequestQueue.offer(request);
    314                     mRequestMap.put(requestId, request);
    315                 } else {
    316                     debugTrace("DownloadRequest #%d was already in the queue");
    317                 }
    318             }
    319             return true;
    320         }
    321 
    322         /**
    323          * This function will remove the specified request from the internal collections.
    324          * @param request The {@link DownloadRequest} that should be removed from our queue
    325          * @return true if it was removed or the request was invalid (meaning that the request
    326          * is not in our queue), false otherwise.
    327          */
    328         public boolean removeRequest(final DownloadRequest request) {
    329             if (request == null) {
    330                 // If it is invalid, its not in the queue.
    331                 return true;
    332             }
    333             debugTrace("Removing DownloadRequest #%d", request.mAttachmentId);
    334             final boolean result;
    335             synchronized (mLock) {
    336                 // It is key to keep the map and queue in lock step
    337                 result = mRequestQueue.remove(request);
    338                 if (result) {
    339                     mRequestMap.remove(request.mAttachmentId);
    340                 }
    341                 return result;
    342             }
    343         }
    344 
    345         /**
    346          * Return the next request from our queue.
    347          * @return The next {@link DownloadRequest} object or null if the queue is empty
    348          */
    349         public DownloadRequest getNextRequest() {
    350             // It is key to keep the map and queue in lock step
    351             final DownloadRequest returnRequest;
    352             synchronized (mLock) {
    353                 returnRequest = mRequestQueue.poll();
    354                 if (returnRequest != null) {
    355                     final long requestId = returnRequest.mAttachmentId;
    356                     mRequestMap.remove(requestId);
    357                 }
    358             }
    359             if (returnRequest != null) {
    360                 debugTrace("Retrieved DownloadRequest #%d", returnRequest.mAttachmentId);
    361             }
    362             return returnRequest;
    363         }
    364 
    365         /**
    366          * Return the {@link DownloadRequest} with the given ID (attachment ID)
    367          * @param requestId The ID of the request in question
    368          * @return The associated {@link DownloadRequest} object or null if it does not exist
    369          */
    370         public DownloadRequest findRequestById(final long requestId) {
    371             if (requestId < 0) {
    372                 return null;
    373             }
    374             synchronized (mLock) {
    375                 return mRequestMap.get(requestId);
    376             }
    377         }
    378 
    379         public int getSize() {
    380             synchronized (mLock) {
    381                 return mRequestMap.size();
    382             }
    383         }
    384 
    385         public boolean isEmpty() {
    386             synchronized (mLock) {
    387                 return mRequestMap.isEmpty();
    388             }
    389         }
    390     }
    391 
    392     /**
    393      * Watchdog alarm receiver; responsible for making sure that downloads in progress are not
    394      * stalled, as determined by the timing of the most recent service callback
    395      */
    396     public static class AttachmentWatchdog extends BroadcastReceiver {
    397         // How often our watchdog checks for callback timeouts
    398         private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS);
    399         public static final String EXTRA_CALLBACK_TIMEOUT = "callback_timeout";
    400         private PendingIntent mWatchdogPendingIntent;
    401 
    402         public void setWatchdogAlarm(final Context context, final long delay,
    403                 final int callbackTimeout) {
    404             // Lazily initialize the pending intent
    405             if (mWatchdogPendingIntent == null) {
    406                 Intent intent = new Intent(context, AttachmentWatchdog.class);
    407                 intent.putExtra(EXTRA_CALLBACK_TIMEOUT, callbackTimeout);
    408                 mWatchdogPendingIntent =
    409                         PendingIntent.getBroadcast(context, 0, intent, 0);
    410             }
    411             // Set the alarm
    412             final AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    413             am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay,
    414                     mWatchdogPendingIntent);
    415             debugTrace("Set up a watchdog for %d millis in the future", delay);
    416         }
    417 
    418         public void setWatchdogAlarm(final Context context) {
    419             // Call the real function with default values.
    420             setWatchdogAlarm(context, WATCHDOG_CHECK_INTERVAL, CALLBACK_TIMEOUT);
    421         }
    422 
    423         @Override
    424         public void onReceive(final Context context, final Intent intent) {
    425             final int callbackTimeout = intent.getIntExtra(EXTRA_CALLBACK_TIMEOUT,
    426                     CALLBACK_TIMEOUT);
    427             new Thread(new Runnable() {
    428                 @Override
    429                 public void run() {
    430                     // TODO: Really don't like hard coding the AttachmentService reference here
    431                     // as it makes testing harder if we are trying to mock out the service
    432                     // We should change this with some sort of getter that returns the
    433                     // static (or test) AttachmentService instance to use.
    434                     final AttachmentService service = AttachmentService.sRunningService;
    435                     if (service != null) {
    436                         // If our service instance is gone, just leave
    437                         if (service.mStop) {
    438                             return;
    439                         }
    440                         // Get the timeout time from the intent.
    441                         watchdogAlarm(service, callbackTimeout);
    442                     }
    443                 }
    444             }, "AttachmentService AttachmentWatchdog").start();
    445         }
    446 
    447         boolean validateDownloadRequest(final DownloadRequest dr, final int callbackTimeout,
    448                 final long now) {
    449             // Check how long it's been since receiving a callback
    450             final long timeSinceCallback = now - dr.mLastCallbackTime;
    451             if (timeSinceCallback > callbackTimeout) {
    452                 LogUtils.d(LOG_TAG, "Timeout for DownloadRequest #%d ", dr.mAttachmentId);
    453                 return true;
    454             }
    455             return false;
    456         }
    457 
    458         /**
    459          * Watchdog for downloads; we use this in case we are hanging on a download, which might
    460          * have failed silently (the connection dropped, for example)
    461          */
    462         void watchdogAlarm(final AttachmentService service, final int callbackTimeout) {
    463             debugTrace("Received a timer callback in the watchdog");
    464 
    465             // We want to iterate on each of the downloads that are currently in progress and
    466             // cancel the ones that seem to be taking too long.
    467             final Collection<DownloadRequest> inProgressRequests =
    468                     service.mDownloadsInProgress.values();
    469             for (DownloadRequest req: inProgressRequests) {
    470                 debugTrace("Checking in-progress request with id: %d", req.mAttachmentId);
    471                 final boolean shouldCancelDownload = validateDownloadRequest(req, callbackTimeout,
    472                         System.currentTimeMillis());
    473                 if (shouldCancelDownload) {
    474                     LogUtils.w(LOG_TAG, "Cancelling DownloadRequest #%d", req.mAttachmentId);
    475                     service.cancelDownload(req);
    476                     // TODO: Should we also mark the attachment as failed at this point in time?
    477                 }
    478             }
    479             // Check whether we can start new downloads...
    480             if (service.isConnected()) {
    481                 service.processQueue();
    482             }
    483             issueNextWatchdogAlarm(service);
    484         }
    485 
    486         void issueNextWatchdogAlarm(final AttachmentService service) {
    487             if (!service.mDownloadsInProgress.isEmpty()) {
    488                 debugTrace("Rescheduling watchdog...");
    489                 setWatchdogAlarm(service);
    490             }
    491         }
    492     }
    493 
    494     /**
    495      * We use an EmailServiceCallback to keep track of the progress of downloads.  These callbacks
    496      * come from either Controller (IMAP/POP) or ExchangeService (EAS).  Note that we only
    497      * implement the single callback that's defined by the EmailServiceCallback interface.
    498      */
    499     class ServiceCallback extends IEmailServiceCallback.Stub {
    500 
    501         /**
    502          * Simple routine to generate updated status values for the Attachment based on the
    503          * service callback. Right now it is very simple but factoring out this code allows us
    504          * to test easier and very easy to expand in the future.
    505          */
    506         ContentValues getAttachmentUpdateValues(final Attachment attachment,
    507                 final int statusCode, final int progress) {
    508             final ContentValues values = new ContentValues();
    509             if (attachment != null) {
    510                 if (statusCode == EmailServiceStatus.IN_PROGRESS) {
    511                     // TODO: What else do we want to expose about this in-progress download through
    512                     // the provider?  If there is more, make sure that the service implementation
    513                     // reports it and make sure that we add it here.
    514                     values.put(AttachmentColumns.UI_STATE, AttachmentState.DOWNLOADING);
    515                     values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
    516                             attachment.mSize * progress / 100);
    517                 }
    518             }
    519             return values;
    520         }
    521 
    522         @Override
    523         public void loadAttachmentStatus(final long messageId, final long attachmentId,
    524                 final int statusCode, final int progress) {
    525             debugTrace(LOG_TAG, "ServiceCallback for attachment #%d", attachmentId);
    526 
    527             // Record status and progress
    528             final DownloadRequest req = mDownloadsInProgress.get(attachmentId);
    529             if (req != null) {
    530                 final long now = System.currentTimeMillis();
    531                 debugTrace("ServiceCallback: status code changing from %d to %d",
    532                         req.mLastStatusCode, statusCode);
    533                 debugTrace("ServiceCallback: progress changing from %d to %d",
    534                         req.mLastProgress,progress);
    535                 debugTrace("ServiceCallback: last callback time changing from %d to %d",
    536                         req.mLastCallbackTime, now);
    537 
    538                 // Update some state to keep track of the progress of the download
    539                 req.mLastStatusCode = statusCode;
    540                 req.mLastProgress = progress;
    541                 req.mLastCallbackTime = now;
    542 
    543                 // Update the attachment status in the provider.
    544                 final Attachment attachment =
    545                         Attachment.restoreAttachmentWithId(AttachmentService.this, attachmentId);
    546                 final ContentValues values = getAttachmentUpdateValues(attachment, statusCode,
    547                         progress);
    548                 if (values.size() > 0) {
    549                     attachment.update(AttachmentService.this, values);
    550                 }
    551 
    552                 switch (statusCode) {
    553                     case EmailServiceStatus.IN_PROGRESS:
    554                         break;
    555                     default:
    556                         // It is assumed that any other error is either a success or an error
    557                         // Either way, the final updates to the DownloadRequest and attachment
    558                         // objects will be handed there.
    559                         LogUtils.d(LOG_TAG, "Attachment #%d is done", attachmentId);
    560                         endDownload(attachmentId, statusCode);
    561                         break;
    562                 }
    563             } else {
    564                 // The only way that we can get a callback from the service implementation for
    565                 // an attachment that doesn't exist is if it was cancelled due to the
    566                 // AttachmentWatchdog. This is a valid scenario and the Watchdog should have already
    567                 // marked this attachment as failed/cancelled.
    568             }
    569         }
    570     }
    571 
    572     /**
    573      * Called directly by EmailProvider whenever an attachment is inserted or changed. Since this
    574      * call is being invoked on the UI thread, we need to make sure that the downloads are
    575      * happening in the background.
    576      * @param context the caller's context
    577      * @param id the attachment's id
    578      * @param flags the new flags for the attachment
    579      */
    580     public static void attachmentChanged(final Context context, final long id, final int flags) {
    581         LogUtils.d(LOG_TAG, "Attachment with id: %d will potentially be queued for download", id);
    582         // Throw this info into an intent and send it to the attachment service.
    583         final Intent intent = new Intent(context, AttachmentService.class);
    584         debugTrace("Calling startService with extras %d & %d", id, flags);
    585         intent.putExtra(EXTRA_ATTACHMENT_ID, id);
    586         intent.putExtra(EXTRA_ATTACHMENT_FLAGS, flags);
    587         context.startService(intent);
    588     }
    589 
    590     /**
    591      * The main entry point for this service, the attachment to download can be identified
    592      * by the EXTRA_ATTACHMENT extra in the intent.
    593      */
    594     @Override
    595     public int onStartCommand(final Intent intent, final int flags, final int startId) {
    596         if (sRunningService == null) {
    597             sRunningService = this;
    598         }
    599         if (intent != null) {
    600             // Let's add this id/flags combo to the list of potential attachments to process.
    601             final long attachment_id = intent.getLongExtra(EXTRA_ATTACHMENT_ID, -1);
    602             final int attachment_flags = intent.getIntExtra(EXTRA_ATTACHMENT_FLAGS, -1);
    603             if ((attachment_id >= 0) && (attachment_flags >= 0)) {
    604                 sAttachmentChangedQueue.add(new long[]{attachment_id, attachment_flags});
    605                 // Process the queue if we're in a wait
    606                 kick();
    607             } else {
    608                 debugTrace("Received an invalid intent w/o the required extras %d & %d",
    609                         attachment_id, attachment_flags);
    610             }
    611         } else {
    612             debugTrace("Received a null intent in onStartCommand");
    613         }
    614         return Service.START_STICKY;
    615     }
    616 
    617     /**
    618      * Most of the leg work is done by our service thread that is created when this
    619      * service is created.
    620      */
    621     @Override
    622     public void onCreate() {
    623         // Start up our service thread.
    624         new Thread(this, "AttachmentService").start();
    625     }
    626 
    627     @Override
    628     public IBinder onBind(final Intent intent) {
    629         return null;
    630     }
    631 
    632     @Override
    633     public void onDestroy() {
    634         debugTrace("Destroying AttachmentService object");
    635         dumpInProgressDownloads();
    636 
    637         // Mark this instance of the service as stopped. Our main loop for the AttachmentService
    638         // checks for this flag along with the AttachmentWatchdog.
    639         mStop = true;
    640         if (sRunningService != null) {
    641             // Kick it awake to get it to realize that we are stopping.
    642             kick();
    643             sRunningService = null;
    644         }
    645         if (mConnectivityManager != null) {
    646             mConnectivityManager.unregister();
    647             mConnectivityManager.stopWait();
    648             mConnectivityManager = null;
    649         }
    650     }
    651 
    652     /**
    653      * The main routine for our AttachmentService service thread.
    654      */
    655     @Override
    656     public void run() {
    657         // These fields are only used within the service thread
    658         mConnectivityManager = new EmailConnectivityManager(this, LOG_TAG);
    659         mAccountManagerStub = new AccountManagerStub(this);
    660 
    661         // Run through all attachments in the database that require download and add them to
    662         // the queue. This is the case where a previous AttachmentService may have been notified
    663         // to stop before processing everything in its queue.
    664         final int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    665         final Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
    666                 EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
    667                 new String[] {Integer.toString(mask)}, null);
    668         try {
    669             LogUtils.d(LOG_TAG,
    670                     "Count of previous downloads to resume (from db): %d", c.getCount());
    671             while (c.moveToNext()) {
    672                 final Attachment attachment = Attachment.restoreAttachmentWithId(
    673                         this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
    674                 if (attachment != null) {
    675                     debugTrace("Attempting to download attachment #%d again.", attachment.mId);
    676                     onChange(this, attachment);
    677                 }
    678             }
    679         } catch (Exception e) {
    680             e.printStackTrace();
    681         } finally {
    682             c.close();
    683         }
    684 
    685         // Loop until stopped, with a 30 minute wait loop
    686         while (!mStop) {
    687             // Here's where we run our attachment loading logic...
    688             // Make a local copy of the variable so we don't null-crash on service shutdown
    689             final EmailConnectivityManager ecm = mConnectivityManager;
    690             if (ecm != null) {
    691                 ecm.waitForConnectivity();
    692             }
    693             if (mStop) {
    694                 // We might be bailing out here due to the service shutting down
    695                 LogUtils.d(LOG_TAG, "AttachmentService has been instructed to stop");
    696                 break;
    697             }
    698 
    699             // In advanced debug mode, let's look at the state of all in-progress downloads
    700             // after processQueue() runs.
    701             debugTrace("Downloads Map before processQueue");
    702             dumpInProgressDownloads();
    703             processQueue();
    704             debugTrace("Downloads Map after processQueue");
    705             dumpInProgressDownloads();
    706 
    707             if (mDownloadQueue.isEmpty() && (mDownloadsInProgress.size() < 1)) {
    708                 LogUtils.d(LOG_TAG, "Shutting down service. No in-progress or pending downloads.");
    709                 stopSelf();
    710                 break;
    711             }
    712             debugTrace("Run() wait for mLock");
    713             synchronized(mLock) {
    714                 try {
    715                     mLock.wait(PROCESS_QUEUE_WAIT_TIME);
    716                 } catch (InterruptedException e) {
    717                     // That's ok; we'll just keep looping
    718                 }
    719             }
    720             debugTrace("Run() got mLock");
    721         }
    722 
    723         // Unregister now that we're done
    724         // Make a local copy of the variable so we don't null-crash on service shutdown
    725         final EmailConnectivityManager ecm = mConnectivityManager;
    726         if (ecm != null) {
    727             ecm.unregister();
    728         }
    729     }
    730 
    731     /*
    732      * Function that kicks the service into action as it may be waiting for this object
    733      * as it processed the last round of attachments.
    734      */
    735     private void kick() {
    736         synchronized(mLock) {
    737             mLock.notify();
    738         }
    739     }
    740 
    741     /**
    742      * onChange is called by the AttachmentReceiver upon receipt of a valid notification from
    743      * EmailProvider that an attachment has been inserted or modified.  It's not strictly
    744      * necessary that we detect a deleted attachment, as the code always checks for the
    745      * existence of an attachment before acting on it.
    746      */
    747     public synchronized void onChange(final Context context, final Attachment att) {
    748         debugTrace("onChange() for Attachment: #%d", att.mId);
    749         DownloadRequest req = mDownloadQueue.findRequestById(att.mId);
    750         final long priority = getAttachmentPriority(att);
    751         if (priority == PRIORITY_NONE) {
    752             LogUtils.d(LOG_TAG, "Attachment #%d has no priority and will not be downloaded",
    753                     att.mId);
    754             // In this case, there is no download priority for this attachment
    755             if (req != null) {
    756                 // If it exists in the map, remove it
    757                 // NOTE: We don't yet support deleting downloads in progress
    758                 mDownloadQueue.removeRequest(req);
    759             }
    760         } else {
    761             // Ignore changes that occur during download
    762             if (mDownloadsInProgress.containsKey(att.mId)) {
    763                 debugTrace("Attachment #%d was already in the queue", att.mId);
    764                 return;
    765             }
    766             // If this is new, add the request to the queue
    767             if (req == null) {
    768                 LogUtils.d(LOG_TAG, "Attachment #%d is a new download request", att.mId);
    769                 req = new DownloadRequest(context, att);
    770                 final AttachmentInfo attachInfo = new AttachmentInfo(context, att);
    771                 if (!attachInfo.isEligibleForDownload()) {
    772                     LogUtils.w(LOG_TAG, "Attachment #%d is not eligible for download", att.mId);
    773                     // We can't download this file due to policy, depending on what type
    774                     // of request we received, we handle the response differently.
    775                     if (((att.mFlags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) ||
    776                             ((att.mFlags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0)) {
    777                         LogUtils.w(LOG_TAG, "Attachment #%d cannot be downloaded ever", att.mId);
    778                         // There are a couple of situations where we will not even allow this
    779                         // request to go in the queue because we can already process it as a
    780                         // failure.
    781                         // 1. The user explicitly wants to download this attachment from the
    782                         // email view but they should not be able to...either because there is
    783                         // no app to view it or because its been marked as a policy violation.
    784                         // 2. The user is forwarding an email and the attachment has been
    785                         // marked as a policy violation. If the attachment is non viewable
    786                         // that is OK for forwarding a message so we'll let it pass through
    787                         markAttachmentAsFailed(att);
    788                         return;
    789                     }
    790                     // If we get this far it a forward of an attachment that is only
    791                     // ineligible because we can't view it or process it. Not because we
    792                     // can't download it for policy reasons. Let's let this go through because
    793                     // the final recipient of this forward email might be able to process it.
    794                 }
    795                 mDownloadQueue.addRequest(req);
    796             }
    797             // TODO: If the request already existed, we'll update the priority (so that the time is
    798             // up-to-date); otherwise, create a new request
    799             LogUtils.d(LOG_TAG,
    800                     "Attachment #%d queued for download, priority: %d, created time: %d",
    801                     att.mId, req.mPriority, req.mCreatedTime);
    802         }
    803         // Process the queue if we're in a wait
    804         kick();
    805     }
    806 
    807     /**
    808      * Set the bits in the provider to mark this download as failed.
    809      * @param att The attachment that failed to download.
    810      */
    811     void markAttachmentAsFailed(final Attachment att) {
    812         final ContentValues cv = new ContentValues();
    813         final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    814         cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
    815         cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED);
    816         att.update(this, cv);
    817     }
    818 
    819     /**
    820      * Set the bits in the provider to mark this download as completed.
    821      * @param att The attachment that was downloaded.
    822      */
    823     void markAttachmentAsCompleted(final Attachment att) {
    824         final ContentValues cv = new ContentValues();
    825         final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
    826         cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
    827         cv.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
    828         att.update(this, cv);
    829     }
    830 
    831     /**
    832      * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
    833      * the limit on maximum downloads
    834      */
    835     synchronized void processQueue() {
    836         debugTrace("Processing changed queue, num entries: %d", sAttachmentChangedQueue.size());
    837 
    838         // First thing we need to do is process the list of "potential downloads" that we
    839         // added to sAttachmentChangedQueue
    840         long[] change = sAttachmentChangedQueue.poll();
    841         while (change != null) {
    842             // Process this change
    843             final long id = change[0];
    844             final long flags = change[1];
    845             final Attachment attachment = Attachment.restoreAttachmentWithId(this, id);
    846             if (attachment == null) {
    847                 LogUtils.w(LOG_TAG, "Could not restore attachment #%d", id);
    848                 continue;
    849             }
    850             attachment.mFlags = (int) flags;
    851             onChange(this, attachment);
    852             change = sAttachmentChangedQueue.poll();
    853         }
    854 
    855         debugTrace("Processing download queue, num entries: %d", mDownloadQueue.getSize());
    856 
    857         while (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS) {
    858             final DownloadRequest req = mDownloadQueue.getNextRequest();
    859             if (req == null) {
    860                 // No more queued requests?  We are done for now.
    861                 break;
    862             }
    863             // Enforce per-account limit here
    864             if (getDownloadsForAccount(req.mAccountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
    865                 LogUtils.w(LOG_TAG, "Skipping #%d; maxed for acct %d",
    866                         req.mAttachmentId, req.mAccountId);
    867                 continue;
    868             }
    869             if (Attachment.restoreAttachmentWithId(this, req.mAttachmentId) == null) {
    870                 LogUtils.e(LOG_TAG, "Could not load attachment: #%d", req.mAttachmentId);
    871                 continue;
    872             }
    873             if (!req.mInProgress) {
    874                 final long currentTime = SystemClock.elapsedRealtime();
    875                 if (req.mRetryCount > 0 && req.mRetryStartTime > currentTime) {
    876                     debugTrace("Need to wait before retrying attachment #%d", req.mAttachmentId);
    877                     mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
    878                             CALLBACK_TIMEOUT);
    879                     continue;
    880                 }
    881                 // TODO: We try to gate ineligible downloads from entering the queue but its
    882                 // always possible that they made it in here regardless in the future.  In a
    883                 // perfect world, we would make it bullet proof with a check for eligibility
    884                 // here instead/also.
    885                 tryStartDownload(req);
    886             }
    887         }
    888 
    889         // Check our ability to be opportunistic regarding background downloads.
    890         final EmailConnectivityManager ecm = mConnectivityManager;
    891         if ((ecm == null) || !ecm.isAutoSyncAllowed() ||
    892                 (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI)) {
    893             // Only prefetch if it if connectivity is available, prefetch is enabled
    894             // and we are on WIFI
    895             LogUtils.d(LOG_TAG, "Skipping opportunistic downloads since WIFI is not available");
    896             return;
    897         }
    898 
    899         // Then, try opportunistic download of appropriate attachments
    900         final int availableBackgroundThreads =
    901                 MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size() - 1;
    902         if (availableBackgroundThreads < 1) {
    903             // We want to leave one spot open for a user requested download that we haven't
    904             // started processing yet.
    905             LogUtils.d(LOG_TAG, "Skipping opportunistic downloads, %d threads available",
    906                     availableBackgroundThreads);
    907             return;
    908         }
    909 
    910         debugTrace("Launching up to %d opportunistic downloads", availableBackgroundThreads);
    911 
    912         // We'll load up the newest 25 attachments that aren't loaded or queued
    913         // TODO: We are always looking for MAX_ATTACHMENTS_TO_CHECK, shouldn't this be
    914         // backgroundDownloads instead?  We should fix and test this.
    915         final Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
    916                 MAX_ATTACHMENTS_TO_CHECK);
    917         final Cursor c = this.getContentResolver().query(lookupUri,
    918                 Attachment.CONTENT_PROJECTION,
    919                 EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
    920                 null, AttachmentColumns._ID + " DESC");
    921         File cacheDir = this.getCacheDir();
    922         try {
    923             while (c.moveToNext()) {
    924                 final Attachment att = new Attachment();
    925                 att.restore(c);
    926                 final Account account = Account.restoreAccountWithId(this, att.mAccountKey);
    927                 if (account == null) {
    928                     // Clean up this orphaned attachment; there's no point in keeping it
    929                     // around; then try to find another one
    930                     debugTrace("Found orphaned attachment #%d", att.mId);
    931                     EmailContent.delete(this, Attachment.CONTENT_URI, att.mId);
    932                 } else {
    933                     // Check that the attachment meets system requirements for download
    934                     // Note that there couple be policy that does not allow this attachment
    935                     // to be downloaded.
    936                     final AttachmentInfo info = new AttachmentInfo(this, att);
    937                     if (info.isEligibleForDownload()) {
    938                         // Either the account must be able to prefetch or this must be
    939                         // an inline attachment.
    940                         if (att.mContentId != null || canPrefetchForAccount(account, cacheDir)) {
    941                             final Integer tryCount = mAttachmentFailureMap.get(att.mId);
    942                             if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) {
    943                                 // move onto the next attachment
    944                                 LogUtils.w(LOG_TAG,
    945                                         "Too many failed attempts for attachment #%d ", att.mId);
    946                                 continue;
    947                             }
    948                             // Start this download and we're done
    949                             final DownloadRequest req = new DownloadRequest(this, att);
    950                             tryStartDownload(req);
    951                             break;
    952                         }
    953                     } else {
    954                         // If this attachment was ineligible for download
    955                         // because of policy related issues, its flags would be set to
    956                         // FLAG_POLICY_DISALLOWS_DOWNLOAD and would not show up in the
    957                         // query results. We are most likely here for other reasons such
    958                         // as the inability to view the attachment. In that case, let's just
    959                         // skip it for now.
    960                         LogUtils.w(LOG_TAG, "Skipping attachment #%d, it is ineligible", att.mId);
    961                     }
    962                 }
    963             }
    964         } finally {
    965             c.close();
    966         }
    967     }
    968 
    969     /**
    970      * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
    971      * parameter
    972      * @param req the DownloadRequest
    973      * @return whether or not the download was started
    974      */
    975     synchronized boolean tryStartDownload(final DownloadRequest req) {
    976         final EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
    977                 AttachmentService.this, req.mAccountId);
    978 
    979         // Do not download the same attachment multiple times
    980         boolean alreadyInProgress = mDownloadsInProgress.get(req.mAttachmentId) != null;
    981         if (alreadyInProgress) {
    982             debugTrace("This attachment #%d is already in progress", req.mAttachmentId);
    983             return false;
    984         }
    985 
    986         try {
    987             startDownload(service, req);
    988         } catch (RemoteException e) {
    989             // TODO: Consider whether we need to do more in this case...
    990             // For now, fix up our data to reflect the failure
    991             cancelDownload(req);
    992         }
    993         return true;
    994     }
    995 
    996     /**
    997      * Do the work of starting an attachment download using the EmailService interface, and
    998      * set our watchdog alarm
    999      *
   1000      * @param service the service handling the download
   1001      * @param req the DownloadRequest
   1002      * @throws RemoteException
   1003      */
   1004     private void startDownload(final EmailServiceProxy service, final DownloadRequest req)
   1005             throws RemoteException {
   1006         LogUtils.d(LOG_TAG, "Starting download for Attachment #%d", req.mAttachmentId);
   1007         req.mStartTime = System.currentTimeMillis();
   1008         req.mInProgress = true;
   1009         mDownloadsInProgress.put(req.mAttachmentId, req);
   1010         service.loadAttachment(mServiceCallback, req.mAccountId, req.mAttachmentId,
   1011                 req.mPriority != PRIORITY_FOREGROUND);
   1012         mWatchdog.setWatchdogAlarm(this);
   1013     }
   1014 
   1015     synchronized void cancelDownload(final DownloadRequest req) {
   1016         LogUtils.d(LOG_TAG, "Cancelling download for Attachment #%d", req.mAttachmentId);
   1017         req.mInProgress = false;
   1018         mDownloadsInProgress.remove(req.mAttachmentId);
   1019         // Remove the download from our queue, and then decide whether or not to add it back.
   1020         mDownloadQueue.removeRequest(req);
   1021         req.mRetryCount++;
   1022         if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) {
   1023             LogUtils.w(LOG_TAG, "Too many failures giving up on Attachment #%d", req.mAttachmentId);
   1024         } else {
   1025             debugTrace("Moving to end of queue, will retry #%d", req.mAttachmentId);
   1026             // The time field of DownloadRequest is final, because it's unsafe to change it
   1027             // as long as the DownloadRequest is in the DownloadSet. It's needed for the
   1028             // comparator, so changing time would make the request unfindable.
   1029             // Instead, we'll create a new DownloadRequest with an updated time.
   1030             // This will sort at the end of the set.
   1031             final DownloadRequest newReq = new DownloadRequest(req, SystemClock.elapsedRealtime());
   1032             mDownloadQueue.addRequest(newReq);
   1033         }
   1034     }
   1035 
   1036     /**
   1037      * Called when a download is finished; we get notified of this via our EmailServiceCallback
   1038      * @param attachmentId the id of the attachment whose download is finished
   1039      * @param statusCode the EmailServiceStatus code returned by the Service
   1040      */
   1041     synchronized void endDownload(final long attachmentId, final int statusCode) {
   1042         LogUtils.d(LOG_TAG, "Finishing download #%d", attachmentId);
   1043 
   1044         // Say we're no longer downloading this
   1045         mDownloadsInProgress.remove(attachmentId);
   1046 
   1047         // TODO: This code is conservative and treats connection issues as failures.
   1048         // Since we have no mechanism to throttle reconnection attempts, it makes
   1049         // sense to be cautious here. Once logic is in place to prevent connecting
   1050         // in a tight loop, we can exclude counting connection issues as "failures".
   1051 
   1052         // Update the attachment failure list if needed
   1053         Integer downloadCount;
   1054         downloadCount = mAttachmentFailureMap.remove(attachmentId);
   1055         if (statusCode != EmailServiceStatus.SUCCESS) {
   1056             if (downloadCount == null) {
   1057                 downloadCount = 0;
   1058             }
   1059             downloadCount += 1;
   1060             LogUtils.w(LOG_TAG, "This attachment failed, adding #%d to failure map", attachmentId);
   1061             mAttachmentFailureMap.put(attachmentId, downloadCount);
   1062         }
   1063 
   1064         final DownloadRequest req = mDownloadQueue.findRequestById(attachmentId);
   1065         if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
   1066             // If this needs to be retried, just process the queue again
   1067             if (req != null) {
   1068                 req.mRetryCount++;
   1069                 if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) {
   1070                     // We are done, we maxed out our total number of tries.
   1071                     // Not that we do not flag this attachment with any special flags so the
   1072                     // AttachmentService will try to download this attachment again the next time
   1073                     // that it starts up.
   1074                     LogUtils.w(LOG_TAG, "Too many tried for connection errors, giving up #%d",
   1075                             attachmentId);
   1076                     mDownloadQueue.removeRequest(req);
   1077                     // Note that we are not doing anything with the attachment right now
   1078                     // We will annotate it later in this function if needed.
   1079                 } else if (req.mRetryCount > CONNECTION_ERROR_DELAY_THRESHOLD) {
   1080                     // TODO: I'm not sure this is a great retry/backoff policy, but we're
   1081                     // afraid of changing behavior too much in case something relies upon it.
   1082                     // So now, for the first five errors, we'll retry immediately. For the next
   1083                     // five tries, we'll add a ten second delay between each. After that, we'll
   1084                     // give up.
   1085                     LogUtils.w(LOG_TAG, "ConnectionError #%d, retried %d times, adding delay",
   1086                             attachmentId, req.mRetryCount);
   1087                     req.mInProgress = false;
   1088                     req.mRetryStartTime = SystemClock.elapsedRealtime() +
   1089                             CONNECTION_ERROR_RETRY_MILLIS;
   1090                     mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
   1091                             CALLBACK_TIMEOUT);
   1092                 } else {
   1093                     LogUtils.w(LOG_TAG, "ConnectionError for #%d, retried %d times, adding delay",
   1094                             attachmentId, req.mRetryCount);
   1095                     req.mInProgress = false;
   1096                     req.mRetryStartTime = 0;
   1097                     kick();
   1098                 }
   1099             }
   1100             return;
   1101         }
   1102 
   1103         // If the request is still in the queue, remove it
   1104         if (req != null) {
   1105             mDownloadQueue.removeRequest(req);
   1106         }
   1107 
   1108         if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) {
   1109             long secs = 0;
   1110             if (req != null) {
   1111                 secs = (System.currentTimeMillis() - req.mCreatedTime) / 1000;
   1112             }
   1113             final String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
   1114                 "Error " + statusCode;
   1115             debugTrace("Download finished for attachment #%d; %d seconds from request, status: %s",
   1116                     attachmentId, secs, status);
   1117         }
   1118 
   1119         final Attachment attachment = Attachment.restoreAttachmentWithId(this, attachmentId);
   1120         if (attachment != null) {
   1121             final long accountId = attachment.mAccountKey;
   1122             // Update our attachment storage for this account
   1123             Long currentStorage = mAttachmentStorageMap.get(accountId);
   1124             if (currentStorage == null) {
   1125                 currentStorage = 0L;
   1126             }
   1127             mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize);
   1128             boolean deleted = false;
   1129             if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
   1130                 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
   1131                     // If this is a forwarding download, and the attachment doesn't exist (or
   1132                     // can't be downloaded) delete it from the outgoing message, lest that
   1133                     // message never get sent
   1134                     EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
   1135                     // TODO: Talk to UX about whether this is even worth doing
   1136                     NotificationController nc = NotificationController.getInstance(this);
   1137                     nc.showDownloadForwardFailedNotificationSynchronous(attachment);
   1138                     deleted = true;
   1139                     LogUtils.w(LOG_TAG, "Deleting forwarded attachment #%d for message #%d",
   1140                         attachmentId, attachment.mMessageKey);
   1141                 }
   1142                 // If we're an attachment on forwarded mail, and if we're not still blocked,
   1143                 // try to send pending mail now (as mediated by MailService)
   1144                 if ((req != null) &&
   1145                         !Utility.hasUnloadedAttachments(this, attachment.mMessageKey)) {
   1146                     debugTrace("Downloads finished for outgoing msg #%d", req.mMessageId);
   1147                     EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
   1148                             this, accountId);
   1149                     try {
   1150                         service.sendMail(accountId);
   1151                     } catch (RemoteException e) {
   1152                         LogUtils.e(LOG_TAG, "RemoteException while trying to send message: #%d, %s",
   1153                                 req.mMessageId, e.toString());
   1154                     }
   1155                 }
   1156             }
   1157             if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
   1158                 Message msg = Message.restoreMessageWithId(this, attachment.mMessageKey);
   1159                 if (msg == null) {
   1160                     LogUtils.w(LOG_TAG, "Deleting attachment #%d with no associated message #%d",
   1161                             attachment.mId, attachment.mMessageKey);
   1162                     // If there's no associated message, delete the attachment
   1163                     EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
   1164                 } else {
   1165                     // If there really is a message, retry
   1166                     // TODO: How will this get retried? It's still marked as inProgress?
   1167                     LogUtils.w(LOG_TAG, "Retrying attachment #%d with associated message #%d",
   1168                             attachment.mId, attachment.mMessageKey);
   1169                     kick();
   1170                     return;
   1171                 }
   1172             } else if (!deleted) {
   1173                 // Clear the download flags, since we're done for now.  Note that this happens
   1174                 // only for non-recoverable errors.  When these occur for forwarded mail, we can
   1175                 // ignore it and continue; otherwise, it was either 1) a user request, in which
   1176                 // case the user can retry manually or 2) an opportunistic download, in which
   1177                 // case the download wasn't critical
   1178                 LogUtils.d(LOG_TAG, "Attachment #%d successfully downloaded!", attachment.mId);
   1179                 markAttachmentAsCompleted(attachment);
   1180             }
   1181         }
   1182         // Process the queue
   1183         kick();
   1184     }
   1185 
   1186     /**
   1187      * Count the number of running downloads in progress for this account
   1188      * @param accountId the id of the account
   1189      * @return the count of running downloads
   1190      */
   1191     synchronized int getDownloadsForAccount(final long accountId) {
   1192         int count = 0;
   1193         for (final DownloadRequest req: mDownloadsInProgress.values()) {
   1194             if (req.mAccountId == accountId) {
   1195                 count++;
   1196             }
   1197         }
   1198         return count;
   1199     }
   1200 
   1201     /**
   1202      * Calculate the download priority of an Attachment.  A priority of zero means that the
   1203      * attachment is not marked for download.
   1204      * @param att the Attachment
   1205      * @return the priority key of the Attachment
   1206      */
   1207     private static int getAttachmentPriority(final Attachment att) {
   1208         int priorityClass = PRIORITY_NONE;
   1209         final int flags = att.mFlags;
   1210         if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
   1211             priorityClass = PRIORITY_SEND_MAIL;
   1212         } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
   1213             priorityClass = PRIORITY_FOREGROUND;
   1214         }
   1215         return priorityClass;
   1216     }
   1217 
   1218     /**
   1219      * Determine whether an attachment can be prefetched for the given account based on
   1220      * total download size restrictions tied to the account.
   1221      * @return true if download is allowed, false otherwise
   1222      */
   1223     public boolean canPrefetchForAccount(final Account account, final File dir) {
   1224         // Check account, just in case
   1225         if (account == null) return false;
   1226 
   1227         // First, check preference and quickly return if prefetch isn't allowed
   1228         if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) {
   1229             debugTrace("Prefetch is not allowed for this account: %d", account.getId());
   1230             return false;
   1231         }
   1232 
   1233         final long totalStorage = dir.getTotalSpace();
   1234         final long usableStorage = dir.getUsableSpace();
   1235         final long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
   1236 
   1237         // If there's not enough overall storage available, stop now
   1238         if (usableStorage < minAvailable) {
   1239             debugTrace("Not enough physical storage for prefetch");
   1240             return false;
   1241         }
   1242 
   1243         final int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
   1244         // Calculate an even per-account storage although it would make a lot of sense to not
   1245         // do this as you may assign more storage to your corporate account rather than a personal
   1246         // account.
   1247         final long perAccountMaxStorage =
   1248                 (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
   1249 
   1250         // Retrieve our idea of currently used attachment storage; since we don't track deletions,
   1251         // this number is the "worst case".  If the number is greater than what's allowed per
   1252         // account, we walk the directory to determine the actual number.
   1253         Long accountStorage = mAttachmentStorageMap.get(account.mId);
   1254         if (accountStorage == null || (accountStorage > perAccountMaxStorage)) {
   1255             // Calculate the exact figure for attachment storage for this account
   1256             accountStorage = 0L;
   1257             File[] files = dir.listFiles();
   1258             if (files != null) {
   1259                 for (File file : files) {
   1260                     accountStorage += file.length();
   1261                 }
   1262             }
   1263             // Cache the value. No locking here since this is a concurrent collection object.
   1264             mAttachmentStorageMap.put(account.mId, accountStorage);
   1265         }
   1266 
   1267         // Return true if we're using less than the maximum per account
   1268         if (accountStorage >= perAccountMaxStorage) {
   1269             debugTrace("Prefetch not allowed for account %d; used: %d, limit %d",
   1270                     account.mId, accountStorage, perAccountMaxStorage);
   1271             return false;
   1272         }
   1273         return true;
   1274     }
   1275 
   1276     boolean isConnected() {
   1277         if (mConnectivityManager != null) {
   1278             return mConnectivityManager.hasConnectivity();
   1279         }
   1280         return false;
   1281     }
   1282 
   1283     // For Debugging.
   1284     synchronized public void dumpInProgressDownloads() {
   1285         if (ENABLE_ATTACHMENT_SERVICE_DEBUG < 1) {
   1286             LogUtils.d(LOG_TAG, "Advanced logging not configured.");
   1287         }
   1288         for (final DownloadRequest req : mDownloadsInProgress.values()) {
   1289             LogUtils.d(LOG_TAG, "--BEGIN DownloadRequest DUMP--");
   1290             LogUtils.d(LOG_TAG, "Account: #%d", req.mAccountId);
   1291             LogUtils.d(LOG_TAG, "Message: #%d", req.mMessageId);
   1292             LogUtils.d(LOG_TAG, "Attachment: #%d", req.mAttachmentId);
   1293             LogUtils.d(LOG_TAG, "Created Time: %d", req.mCreatedTime);
   1294             LogUtils.d(LOG_TAG, "Priority: %d", req.mPriority);
   1295             if (req.mInProgress == true) {
   1296                 LogUtils.d(LOG_TAG, "This download is in progress");
   1297             } else {
   1298                 LogUtils.d(LOG_TAG, "This download is not in progress");
   1299             }
   1300             LogUtils.d(LOG_TAG, "Start Time: %d", req.mStartTime);
   1301             LogUtils.d(LOG_TAG, "Retry Count: %d", req.mRetryCount);
   1302             LogUtils.d(LOG_TAG, "Retry Start Tiome: %d", req.mRetryStartTime);
   1303             LogUtils.d(LOG_TAG, "Last Status Code: %d", req.mLastStatusCode);
   1304             LogUtils.d(LOG_TAG, "Last Progress: %d", req.mLastProgress);
   1305             LogUtils.d(LOG_TAG, "Last Callback Time: %d", req.mLastCallbackTime);
   1306             LogUtils.d(LOG_TAG, "------------------------------");
   1307         }
   1308     }
   1309 
   1310 
   1311     @Override
   1312     public void dump(final FileDescriptor fd, final PrintWriter pw, final String[] args) {
   1313         pw.println("AttachmentService");
   1314         final long time = System.currentTimeMillis();
   1315         synchronized(mDownloadQueue) {
   1316             pw.println("  Queue, " + mDownloadQueue.getSize() + " entries");
   1317             // If you iterate over the queue either via iterator or collection, they are not
   1318             // returned in any particular order. With all things being equal its better to go with
   1319             // a collection to avoid any potential ConcurrentModificationExceptions.
   1320             // If we really want this sorted, we can sort it manually since performance isn't a big
   1321             // concern with this debug method.
   1322             for (final DownloadRequest req : mDownloadQueue.mRequestMap.values()) {
   1323                 pw.println("    Account: " + req.mAccountId + ", Attachment: " + req.mAttachmentId);
   1324                 pw.println("      Priority: " + req.mPriority + ", Time: " + req.mCreatedTime +
   1325                         (req.mInProgress ? " [In progress]" : ""));
   1326                 final Attachment att = Attachment.restoreAttachmentWithId(this, req.mAttachmentId);
   1327                 if (att == null) {
   1328                     pw.println("      Attachment not in database?");
   1329                 } else if (att.mFileName != null) {
   1330                     final String fileName = att.mFileName;
   1331                     final String suffix;
   1332                     final int lastDot = fileName.lastIndexOf('.');
   1333                     if (lastDot >= 0) {
   1334                         suffix = fileName.substring(lastDot);
   1335                     } else {
   1336                         suffix = "[none]";
   1337                     }
   1338                     pw.print("      Suffix: " + suffix);
   1339                     if (att.getContentUri() != null) {
   1340                         pw.print(" ContentUri: " + att.getContentUri());
   1341                     }
   1342                     pw.print(" Mime: ");
   1343                     if (att.mMimeType != null) {
   1344                         pw.print(att.mMimeType);
   1345                     } else {
   1346                         pw.print(AttachmentUtilities.inferMimeType(fileName, null));
   1347                         pw.print(" [inferred]");
   1348                     }
   1349                     pw.println(" Size: " + att.mSize);
   1350                 }
   1351                 if (req.mInProgress) {
   1352                     pw.println("      Status: " + req.mLastStatusCode + ", Progress: " +
   1353                             req.mLastProgress);
   1354                     pw.println("      Started: " + req.mStartTime + ", Callback: " +
   1355                             req.mLastCallbackTime);
   1356                     pw.println("      Elapsed: " + ((time - req.mStartTime) / 1000L) + "s");
   1357                     if (req.mLastCallbackTime > 0) {
   1358                         pw.println("      CB: " + ((time - req.mLastCallbackTime) / 1000L) + "s");
   1359                     }
   1360                 }
   1361             }
   1362         }
   1363     }
   1364 
   1365     // For Testing
   1366     AccountManagerStub mAccountManagerStub;
   1367     private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
   1368 
   1369     void addServiceIntentForTest(final long accountId, final Intent intent) {
   1370         mAccountServiceMap.put(accountId, intent);
   1371     }
   1372 
   1373     /**
   1374      * We only use the getAccounts() call from AccountManager, so this class wraps that call and
   1375      * allows us to build a mock account manager stub in the unit tests
   1376      */
   1377     static class AccountManagerStub {
   1378         private int mNumberOfAccounts;
   1379         private final AccountManager mAccountManager;
   1380 
   1381         AccountManagerStub(final Context context) {
   1382             if (context != null) {
   1383                 mAccountManager = AccountManager.get(context);
   1384             } else {
   1385                 mAccountManager = null;
   1386             }
   1387         }
   1388 
   1389         int getNumberOfAccounts() {
   1390             if (mAccountManager != null) {
   1391                 return mAccountManager.getAccounts().length;
   1392             } else {
   1393                 return mNumberOfAccounts;
   1394             }
   1395         }
   1396 
   1397         void setNumberOfAccounts(final int numberOfAccounts) {
   1398             mNumberOfAccounts = numberOfAccounts;
   1399         }
   1400     }
   1401 }
   1402