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