AttachmentDownloadService.java revision 69fc25244ba1b30856426c77c2e4be3964eb50da
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.Controller.ControllerService;
20import com.android.email.Email;
21import com.android.email.ExchangeUtils.NullEmailService;
22import com.android.email.NotificationController;
23import com.android.email.Utility;
24import com.android.email.provider.AttachmentProvider;
25import com.android.email.provider.EmailContent;
26import com.android.email.provider.EmailContent.Account;
27import com.android.email.provider.EmailContent.Attachment;
28import com.android.email.provider.EmailContent.Message;
29import com.android.exchange.ExchangeService;
30
31import android.app.AlarmManager;
32import android.app.PendingIntent;
33import android.app.Service;
34import android.content.BroadcastReceiver;
35import android.content.ContentValues;
36import android.content.Context;
37import android.content.Intent;
38import android.database.Cursor;
39import android.os.IBinder;
40import android.os.RemoteException;
41import android.text.format.DateUtils;
42import android.util.Log;
43
44import java.io.File;
45import java.io.FileDescriptor;
46import java.io.PrintWriter;
47import java.util.Comparator;
48import java.util.HashMap;
49import java.util.Iterator;
50import java.util.TreeSet;
51import java.util.concurrent.ConcurrentHashMap;
52
53public class AttachmentDownloadService extends Service implements Runnable {
54    public static final String TAG = "AttachmentService";
55
56    // Our idle time, waiting for notifications; this is something of a failsafe
57    private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS);
58    // How often our watchdog checks for callback timeouts
59    private static final int WATCHDOG_CHECK_INTERVAL = 15 * ((int)DateUtils.SECOND_IN_MILLIS);
60    // How long we'll wait for a callback before canceling a download and retrying
61    private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS);
62
63    private static final int PRIORITY_NONE = -1;
64    @SuppressWarnings("unused")
65    // Low priority will be used for opportunistic downloads
66    private static final int PRIORITY_LOW = 0;
67    // Normal priority is for forwarded downloads in outgoing mail
68    private static final int PRIORITY_NORMAL = 1;
69    // High priority is for user requests
70    private static final int PRIORITY_HIGH = 2;
71
72    // We can try various values here; I think 2 is completely reasonable as a first pass
73    private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
74    // Limit on the number of simultaneous downloads per account
75    // Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
76    private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
77
78    /*package*/ static AttachmentDownloadService sRunningService = null;
79
80    /*package*/ Context mContext;
81    /*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator());
82
83    private final HashMap<Long, Class<? extends Service>> mAccountServiceMap =
84        new HashMap<Long, Class<? extends Service>>();
85    private final ServiceCallback mServiceCallback = new ServiceCallback();
86    private final Object mLock = new Object();
87    private volatile boolean mStop = false;
88
89
90    /**
91     * Watchdog alarm receiver; responsible for making sure that downloads in progress are not
92     * stalled, as determined by the timing of the most recent service callback
93     */
94    public static class Watchdog extends BroadcastReceiver {
95        @Override
96        public void onReceive(final Context context, Intent intent) {
97            new Thread(new Runnable() {
98                public void run() {
99                    watchdogAlarm();
100                }
101            }, "AttachmentDownloadService Watchdog").start();
102        }
103    }
104
105    public static class DownloadRequest {
106        final int priority;
107        final long time;
108        final long attachmentId;
109        final long messageId;
110        final long accountId;
111        boolean inProgress = false;
112        int lastStatusCode;
113        int lastProgress;
114        long lastCallbackTime;
115        long startTime;
116
117        private DownloadRequest(Context context, Attachment attachment) {
118            attachmentId = attachment.mId;
119            Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
120            if (msg != null) {
121                accountId = msg.mAccountKey;
122                messageId = msg.mId;
123            } else {
124                accountId = messageId = -1;
125            }
126            priority = getPriority(attachment);
127            time = System.currentTimeMillis();
128        }
129
130        @Override
131        public int hashCode() {
132            return (int)attachmentId;
133        }
134
135        /**
136         * Two download requests are equals if their attachment id's are equals
137         */
138        @Override
139        public boolean equals(Object object) {
140            if (!(object instanceof DownloadRequest)) return false;
141            DownloadRequest req = (DownloadRequest)object;
142            return req.attachmentId == attachmentId;
143        }
144    }
145
146    /**
147     * Comparator class for the download set; we first compare by priority.  Requests with equal
148     * priority are compared by the time the request was created (older requests come first)
149     */
150    /*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> {
151        @Override
152        public int compare(DownloadRequest req1, DownloadRequest req2) {
153            int res;
154            if (req1.priority != req2.priority) {
155                res = (req1.priority < req2.priority) ? -1 : 1;
156            } else {
157                if (req1.time == req2.time) {
158                    res = 0;
159                } else {
160                    res = (req1.time > req2.time) ? -1 : 1;
161                }
162            }
163            //Log.d(TAG, "Compare " + req1.attachmentId + " to " + req2.attachmentId + " = " + res);
164            return res;
165        }
166    }
167
168    /**
169     * The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the
170     * time of the request.  Higher priority requests
171     * are always processed first; among equals, the oldest request is processed first.  The
172     * priority key represents this ordering.  Note: All methods that change the attachment map are
173     * synchronized on the map itself
174     */
175    /*package*/ class DownloadSet extends TreeSet<DownloadRequest> {
176        private static final long serialVersionUID = 1L;
177        private PendingIntent mWatchdogPendingIntent;
178        private AlarmManager mAlarmManager;
179
180        /*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) {
181            super(comparator);
182        }
183
184        /**
185         * Maps attachment id to DownloadRequest
186         */
187        /*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
188            new ConcurrentHashMap<Long, DownloadRequest>();
189
190        /**
191         * onChange is called by the AttachmentReceiver upon receipt of a valid notification from
192         * EmailProvider that an attachment has been inserted or modified.  It's not strictly
193         * necessary that we detect a deleted attachment, as the code always checks for the
194         * existence of an attachment before acting on it.
195         */
196        public synchronized void onChange(Attachment att) {
197            DownloadRequest req = findDownloadRequest(att.mId);
198            long priority = getPriority(att);
199            if (priority == PRIORITY_NONE) {
200                if (Email.DEBUG) {
201                    Log.d(TAG, "== Attachment changed: " + att.mId);
202                }
203                // In this case, there is no download priority for this attachment
204                if (req != null) {
205                    // If it exists in the map, remove it
206                    // NOTE: We don't yet support deleting downloads in progress
207                    if (Email.DEBUG) {
208                        Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
209                    }
210                    remove(req);
211                }
212            } else {
213                // Ignore changes that occur during download
214                if (mDownloadsInProgress.containsKey(att.mId)) return;
215                // If this is new, add the request to the queue
216                if (req == null) {
217                    req = new DownloadRequest(mContext, att);
218                    add(req);
219                }
220                // If the request already existed, we'll update the priority (so that the time is
221                // up-to-date); otherwise, we create a new request
222                if (Email.DEBUG) {
223                    Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
224                            req.priority + ", priority time " + req.time);
225                }
226            }
227            // Process the queue if we're in a wait
228            kick();
229        }
230
231        /**
232         * Find a queued DownloadRequest, given the attachment's id
233         * @param id the id of the attachment
234         * @return the DownloadRequest for that attachment (or null, if none)
235         */
236        /*package*/ synchronized DownloadRequest findDownloadRequest(long id) {
237            Iterator<DownloadRequest> iterator = iterator();
238            while(iterator.hasNext()) {
239                DownloadRequest req = iterator.next();
240                if (req.attachmentId == id) {
241                    return req;
242                }
243            }
244            return null;
245        }
246
247        /**
248         * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
249         * the limit on maximum downloads
250         */
251        /*package*/ synchronized void processQueue() {
252            if (Email.DEBUG) {
253                Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries");
254            }
255            Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
256            // First, start up any required downloads, in priority order
257            while (iterator.hasNext() &&
258                    (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
259                DownloadRequest req = iterator.next();
260                if (!req.inProgress) {
261                    mDownloadSet.tryStartDownload(req);
262                }
263            }
264            // Then, try opportunistic download of appropriate attachments
265            int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
266            if (backgroundDownloads > 0) {
267                // TODO Code for background downloads here
268                if (Email.DEBUG) {
269                    Log.d(TAG, "== We'd look for up to " + backgroundDownloads +
270                            " background download(s) now...");
271                }
272            }
273        }
274
275        /**
276         * Count the number of running downloads in progress for this account
277         * @param accountId the id of the account
278         * @return the count of running downloads
279         */
280        /*package*/ synchronized int downloadsForAccount(long accountId) {
281            int count = 0;
282            for (DownloadRequest req: mDownloadsInProgress.values()) {
283                if (req.accountId == accountId) {
284                    count++;
285                }
286            }
287            return count;
288        }
289
290        private void onWatchdogAlarm() {
291            long now = System.currentTimeMillis();
292            for (DownloadRequest req: mDownloadsInProgress.values()) {
293                // Check how long it's been since receiving a callback
294                long timeSinceCallback = now - req.lastCallbackTime;
295                if (timeSinceCallback > CALLBACK_TIMEOUT) {
296                    if (Email.DEBUG) {
297                        Log.d(TAG, "== ,  Download of " + req.attachmentId +
298                                " timed out");
299                    }
300                   cancelDownload(req);
301                // STOPSHIP Remove this before ship
302                } else if (Email.DEBUG) {
303                    Log.d(TAG, "== ,  Download of " + req.attachmentId +
304                    " last callback " + (timeSinceCallback/1000) + "  secs ago");
305                }
306            }
307            // If there are downloads in progress, reset alarm
308            if (mDownloadsInProgress.isEmpty()) {
309                if (mAlarmManager != null && mWatchdogPendingIntent != null) {
310                    mAlarmManager.cancel(mWatchdogPendingIntent);
311                }
312            }
313            // Check whether we can start new downloads...
314            processQueue();
315        }
316
317        /**
318         * Do the work of starting an attachment download using the EmailService interface, and
319         * set our watchdog alarm
320         *
321         * @param serviceClass the class that will attempt the download
322         * @param req the DownloadRequest
323         * @throws RemoteException
324         */
325        private void startDownload(Class<? extends Service> serviceClass, DownloadRequest req)
326                throws RemoteException {
327            File file = AttachmentProvider.getAttachmentFilename(mContext, req.accountId,
328                    req.attachmentId);
329            req.startTime = System.currentTimeMillis();
330            req.inProgress = true;
331            mDownloadsInProgress.put(req.attachmentId, req);
332            if (serviceClass.equals(NullEmailService.class)) return;
333            // Now, call the service
334            EmailServiceProxy proxy =
335                new EmailServiceProxy(mContext, serviceClass, mServiceCallback);
336            proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(),
337                    AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId)
338                    .toString());
339            // Lazily initialize our (reusable) pending intent
340            if (mWatchdogPendingIntent == null) {
341                Intent alarmIntent = new Intent(mContext, Watchdog.class);
342                mWatchdogPendingIntent = PendingIntent.getBroadcast(mContext, 0, alarmIntent, 0);
343                mAlarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
344            }
345            // Set the alarm
346            mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
347                    System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, WATCHDOG_CHECK_INTERVAL,
348                    mWatchdogPendingIntent);
349        }
350
351        /**
352         * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
353         * parameter
354         * @param req the DownloadRequest
355         * @return whether or not the download was started
356         */
357        /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
358            // Enforce per-account limit
359            if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
360                if (Email.DEBUG) {
361                    Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
362                            req.accountId);
363                }
364                return false;
365            }
366            Class<? extends Service> serviceClass = getServiceClassForAccount(req.accountId);
367            if (serviceClass == null) return false;
368            try {
369                if (Email.DEBUG) {
370                    Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
371                }
372                startDownload(serviceClass, req);
373            } catch (RemoteException e) {
374                // TODO: Consider whether we need to do more in this case...
375                // For now, fix up our data to reflect the failure
376                cancelDownload(req);
377            }
378            return true;
379        }
380
381        private void cancelDownload(DownloadRequest req) {
382            mDownloadsInProgress.remove(req.attachmentId);
383            req.inProgress = false;
384        }
385
386        /**
387         * Called when a download is finished; we get notified of this via our EmailServiceCallback
388         * @param attachmentId the id of the attachment whose download is finished
389         * @param statusCode the EmailServiceStatus code returned by the Service
390         */
391        /*package*/ synchronized void endDownload(long attachmentId, int statusCode) {
392            // Say we're no longer downloading this
393            mDownloadsInProgress.remove(attachmentId);
394            DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
395            if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
396                // If this needs to be retried, just process the queue again
397                if (Email.DEBUG) {
398                    Log.d(TAG, "== The download for attachment #" + attachmentId +
399                            " will be retried");
400                }
401                if (req != null) {
402                    req.inProgress = false;
403                }
404                kick();
405                return;
406            }
407
408            // Remove the request from the queue
409            remove(req);
410            if (Email.DEBUG) {
411                long secs = 0;
412                if (req != null) {
413                    secs = (System.currentTimeMillis() - req.time) / 1000;
414                }
415                String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
416                    "Error " + statusCode;
417                Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs +
418                           " seconds from request, status: " + status);
419            }
420
421            Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
422            if (attachment != null) {
423                boolean deleted = false;
424                if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
425                    if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
426                        // If this is a forwarding download, and the attachment doesn't exist (or
427                        // can't be downloaded) delete it from the outgoing message, lest that
428                        // message never get sent
429                        EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
430                        // TODO: Talk to UX about whether this is even worth doing
431                        NotificationController nc = NotificationController.getInstance(mContext);
432                        nc.showDownloadForwardFailedNotification(attachment);
433                        deleted = true;
434                    }
435                    // If we're an attachment on forwarded mail, and if we're not still blocked,
436                    // try to send pending mail now (as mediated by MailService)
437                    if ((req != null) &&
438                            !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
439                        if (Email.DEBUG) {
440                            Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId);
441                        }
442                        MailService.actionSendPendingMail(mContext, req.accountId);
443                    }
444                }
445                if (!deleted) {
446                    // Clear the download flags, since we're done for now.  Note that this happens
447                    // only for non-recoverable errors.  When these occur for forwarded mail, we can
448                    // ignore it and continue; otherwise, it was either 1) a user request, in which
449                    // case the user can retry manually or 2) an opportunistic download, in which
450                    // case the download wasn't critical
451                    ContentValues cv = new ContentValues();
452                    int flags =
453                        Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
454                    cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags);
455                    attachment.update(mContext, cv);
456                }
457            }
458            // Process the queue
459            kick();
460        }
461    }
462
463    /**
464     * Calculate the download priority of an Attachment.  A priority of zero means that the
465     * attachment is not marked for download.
466     * @param att the Attachment
467     * @return the priority key of the Attachment
468     */
469    private static int getPriority(Attachment att) {
470        int priorityClass = PRIORITY_NONE;
471        int flags = att.mFlags;
472        if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
473            priorityClass = PRIORITY_NORMAL;
474        } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
475            priorityClass = PRIORITY_HIGH;
476        }
477        return priorityClass;
478    }
479
480    private void kick() {
481        synchronized(mLock) {
482            mLock.notify();
483        }
484    }
485
486    /**
487     * We use an EmailServiceCallback to keep track of the progress of downloads.  These callbacks
488     * come from either Controller (IMAP) or ExchangeService (EAS).  Note that we only implement the
489     * single callback that's defined by the EmailServiceCallback interface.
490     */
491    private class ServiceCallback extends IEmailServiceCallback.Stub {
492        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
493                int progress) {
494            if (Email.DEBUG) {
495                String code;
496                switch(statusCode) {
497                    case EmailServiceStatus.SUCCESS:
498                        code = "Success";
499                        break;
500                    case EmailServiceStatus.IN_PROGRESS:
501                        code = "In progress";
502                        break;
503                    default:
504                        code = Integer.toString(statusCode);
505                }
506                Log.d(TAG, "loadAttachmentStatus, id = " + attachmentId + " code = "+ code +
507                        ", " + progress + "%");
508            }
509            // Record status and progress
510            DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
511            if (req != null) {
512                req.lastStatusCode = statusCode;
513                req.lastProgress = progress;
514                req.lastCallbackTime = System.currentTimeMillis();
515            }
516            switch (statusCode) {
517                case EmailServiceStatus.IN_PROGRESS:
518                    break;
519                default:
520                    mDownloadSet.endDownload(attachmentId, statusCode);
521                    break;
522            }
523        }
524
525        @Override
526        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
527                throws RemoteException {
528        }
529
530        @Override
531        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
532                throws RemoteException {
533        }
534
535        @Override
536        public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
537                throws RemoteException {
538        }
539    }
540
541    /**
542     * Return the class of the service used by the account type of the provided account id.  We
543     * cache the results to avoid repeated database access
544     * @param accountId the id of the account
545     * @return the service class for the account
546     */
547    private synchronized Class<? extends Service> getServiceClassForAccount(long accountId) {
548        // TODO: We should have some more data-driven way of determining the service class. I'd
549        // suggest adding an attribute in the stores.xml file
550        Class<? extends Service> serviceClass = mAccountServiceMap.get(accountId);
551        if (serviceClass == null) {
552            String protocol = Account.getProtocol(mContext, accountId);
553            if (protocol.equals("eas")) {
554                serviceClass = ExchangeService.class;
555            } else {
556                serviceClass = ControllerService.class;
557            }
558            mAccountServiceMap.put(accountId, serviceClass);
559        }
560        return serviceClass;
561    }
562
563    /*protected*/ void addServiceClass(long accountId, Class<? extends Service> serviceClass) {
564        mAccountServiceMap.put(accountId, serviceClass);
565    }
566
567    /*package*/ void onChange(Attachment att) {
568        mDownloadSet.onChange(att);
569    }
570
571    /*package*/ boolean isQueued(long attachmentId) {
572        return mDownloadSet.findDownloadRequest(attachmentId) != null;
573    }
574
575    /*package*/ int getSize() {
576        return mDownloadSet.size();
577    }
578
579    /*package*/ boolean dequeue(long attachmentId) {
580        DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
581        if (req != null) {
582            if (Email.DEBUG) {
583                Log.d(TAG, "Dequeued attachmentId:  " + attachmentId);
584            }
585            mDownloadSet.remove(req);
586            return true;
587        }
588        return false;
589    }
590
591    /**
592     * Ask the service for the number of items in the download queue
593     * @return the number of items queued for download
594     */
595    public static int getQueueSize() {
596        if (sRunningService != null) {
597            return sRunningService.getSize();
598        }
599        return 0;
600    }
601
602    /**
603     * Ask the service whether a particular attachment is queued for download
604     * @param attachmentId the id of the Attachment (as stored by EmailProvider)
605     * @return whether or not the attachment is queued for download
606     */
607    public static boolean isAttachmentQueued(long attachmentId) {
608        if (sRunningService != null) {
609            return sRunningService.isQueued(attachmentId);
610        }
611        return false;
612    }
613
614    /**
615     * Ask the service to remove an attachment from the download queue
616     * @param attachmentId the id of the Attachment (as stored by EmailProvider)
617     * @return whether or not the attachment was removed from the queue
618     */
619    public static boolean cancelQueuedAttachment(long attachmentId) {
620        if (sRunningService != null) {
621            return sRunningService.dequeue(attachmentId);
622        }
623        return false;
624    }
625
626    public static void watchdogAlarm() {
627        if (sRunningService != null) {
628            sRunningService.mDownloadSet.onWatchdogAlarm();
629        }
630    }
631
632    /**
633     * Called directly by EmailProvider whenever an attachment is inserted or changed
634     * @param id the attachment's id
635     * @param flags the new flags for the attachment
636     */
637    public static void attachmentChanged(final long id, final int flags) {
638        if (sRunningService == null) return;
639        Utility.runAsync(new Runnable() {
640            public void run() {
641                final Attachment attachment =
642                    Attachment.restoreAttachmentWithId(sRunningService, id);
643                if (attachment != null) {
644                    // Store the flags we got from EmailProvider; given that all of this
645                    // activity is asynchronous, we need to use the newest data from
646                    // EmailProvider
647                    attachment.mFlags = flags;
648                    sRunningService.onChange(attachment);
649                }
650            }});
651    }
652
653    public void run() {
654        mContext = this;
655        // Run through all attachments in the database that require download and add them to
656        // the queue
657        int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
658        Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
659                EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0",
660                new String[] {Integer.toString(mask)}, null);
661        try {
662            Log.d(TAG, "Count: " + c.getCount());
663            while (c.moveToNext()) {
664                Attachment attachment = Attachment.restoreAttachmentWithId(
665                        this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
666                if (attachment != null) {
667                    mDownloadSet.onChange(attachment);
668                }
669            }
670        } catch (Exception e) {
671            e.printStackTrace();
672        }
673        finally {
674            c.close();
675        }
676
677        // Loop until stopped, with a 30 minute wait loop
678        while (!mStop) {
679            // Here's where we run our attachment loading logic...
680            mDownloadSet.processQueue();
681            synchronized(mLock) {
682                try {
683                    mLock.wait(PROCESS_QUEUE_WAIT_TIME);
684                } catch (InterruptedException e) {
685                    // That's ok; we'll just keep looping
686                }
687            }
688        }
689    }
690
691    @Override
692    public int onStartCommand(Intent intent, int flags, int startId) {
693        sRunningService = this;
694        return Service.START_STICKY;
695    }
696
697    /**
698     * The lifecycle of this service is managed by Email.setServicesEnabled(), which is called
699     * throughout the code, in particular 1) after boot and 2) after accounts are added or removed
700     * The goal is that this service should be running at all times when there's at least one
701     * email account present.
702     */
703    @Override
704    public void onCreate() {
705        // Start up our service thread
706        new Thread(this, "AttachmentDownloadService").start();
707    }
708    @Override
709    public IBinder onBind(Intent intent) {
710        return null;
711    }
712
713    @Override
714    public void onDestroy() {
715        Log.d(TAG, "**** ON DESTROY!");
716        if (sRunningService != null) {
717            mStop = true;
718            kick();
719        }
720        sRunningService = null;
721    }
722
723    @Override
724    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
725        pw.println("AttachmentDownloadService");
726        long time = System.currentTimeMillis();
727        synchronized(mDownloadSet) {
728            pw.println("  Queue, " + mDownloadSet.size() + " entries");
729            Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
730            // First, start up any required downloads, in priority order
731            while (iterator.hasNext()) {
732                DownloadRequest req = iterator.next();
733                pw.println("    Account: " + req.accountId + ", Attachment: " + req.attachmentId);
734                pw.println("      Priority: " + req.priority + ", Time: " + req.time +
735                        (req.inProgress ? " [In progress]" : ""));
736                Attachment att = Attachment.restoreAttachmentWithId(mContext, req.attachmentId);
737                if (att == null) {
738                    pw.println("      Attachment not in database?");
739                } else if (att.mFileName != null) {
740                    String fileName = att.mFileName;
741                    String suffix = "[none]";
742                    int lastDot = fileName.lastIndexOf('.');
743                    if (lastDot >= 0) {
744                        suffix = fileName.substring(lastDot);
745                    }
746                    pw.print("      Suffix: " + suffix);
747                    if (att.mContentUri != null) {
748                        pw.print(" ContentUri: " + att.mContentUri);
749                    }
750                    pw.print(" Mime: ");
751                    if (att.mMimeType != null) {
752                        pw.print(att.mMimeType);
753                    } else {
754                        pw.print(AttachmentProvider.inferMimeType(fileName, null));
755                        pw.print(" [inferred]");
756                    }
757                    pw.println(" Size: " + att.mSize);
758                }
759                if (req.inProgress) {
760                    pw.println("      Status: " + req.lastStatusCode + ", Progress: " +
761                            req.lastProgress);
762                    pw.println("      Started: " + req.startTime + ", Callback: " +
763                            req.lastCallbackTime);
764                    pw.println("      Elapsed: " + ((time - req.startTime) / 1000L) + "s");
765                    if (req.lastCallbackTime > 0) {
766                        pw.println("      CB: " + ((time - req.lastCallbackTime) / 1000L) + "s");
767                    }
768                }
769            }
770        }
771    }
772}
773