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