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