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