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