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