AttachmentDownloadService.java revision 7894ee82b3a9f22d460a0c6f79e87be27686a649
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.Email;
20import com.android.email.R;
21import com.android.email.Utility;
22import com.android.email.activity.Welcome;
23import com.android.email.provider.AttachmentProvider;
24import com.android.email.provider.EmailContent;
25import com.android.email.provider.EmailProvider;
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.SyncManager;
30
31import android.app.Notification;
32import android.app.NotificationManager;
33import android.app.PendingIntent;
34import android.app.Service;
35import android.content.BroadcastReceiver;
36import android.content.ContentValues;
37import android.content.Context;
38import android.content.Intent;
39import android.content.IntentFilter;
40import android.content.IntentFilter.MalformedMimeTypeException;
41import android.database.Cursor;
42import android.net.Uri;
43import android.os.IBinder;
44import android.os.RemoteException;
45import android.text.format.DateUtils;
46import android.util.Log;
47import android.widget.RemoteViews;
48
49import java.io.File;
50import java.util.HashMap;
51import java.util.TreeMap;
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
59    private static final long PRIORITY_NONE = -1;
60    @SuppressWarnings("unused")
61    private static final long PRIORITY_LOW = 0;
62    private static final long PRIORITY_NORMAL = 1;
63    private static final long PRIORITY_HIGH = 2;
64
65    // We can try various values here; I think 2 is completely reasonable as a first pass
66    private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
67    // Limit on the number of simultaneous downloads per account
68    // Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
69    private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
70
71    private static AttachmentDownloadService sRunningService = null;
72
73    private AttachmentReceiver mAttachmentReceiver;
74    private Context mContext;
75    private final AttachmentMap mAttachmentMap = new AttachmentMap();
76    private final HashMap<Long, Class<? extends Service>> mAccountServiceMap =
77        new HashMap<Long, Class<? extends Service>>();
78    private final ServiceCallback mServiceCallback = new ServiceCallback();
79    private final Object mLock = new Object();
80    private volatile boolean mStop = false;
81
82    private static class DownloadRequest {
83        long attachmentId;
84        long messageId = -1;
85        long accountId;
86        boolean inProgress = false;
87
88        private DownloadRequest(Context context, Attachment attachment) {
89            attachmentId = attachment.mId;
90            Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
91            if (msg != null) {
92                accountId = msg.mAccountKey;
93                messageId = msg.mId;
94            }
95        }
96    }
97
98    /**
99     * The AttachmentMap is a TreeMap sorted by "priority key", which is determined first by the
100     * priority class (e.g. low, high, etc.) and the time of the request.  Higher priority requests
101     * are always processed first; among equals, the oldest request is processed first.  The
102     * priority key represents this ordering.  Note: All methods that change the attachment map are
103     * synchronized on the map itself
104     */
105    private class AttachmentMap extends TreeMap<Long, DownloadRequest> {
106        private static final long serialVersionUID = 1L;
107
108        private final HashMap<Long, DownloadRequest> mDownloadsInProgress =
109            new HashMap<Long, DownloadRequest>();
110
111        /**
112         * Calculate the download priority of an Attachment.  A priority of zero means that the
113         * attachment is not marked for download.
114         * @param att the Attachment
115         * @return the priority key of the Attachment
116         */
117        private long getPriorityKey(Attachment att) {
118            long priorityClass = PRIORITY_NONE;
119            int flags = att.mFlags;
120            if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
121                priorityClass = PRIORITY_NORMAL;
122            } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
123                priorityClass = PRIORITY_HIGH;
124            } else {
125                return 0;
126            }
127            return (priorityKey(priorityClass, System.currentTimeMillis()));
128        }
129
130        /**
131         * onChange is called by the AttachmentReceiver upon receipt of a valid notification from
132         * EmailProvider that an attachment has been inserted or modified.  It's not strictly
133         * necessary that we detect a deleted attachment, as the code always checks for the
134         * existence of an attachment before acting on it.
135         */
136        public synchronized void onChange(Attachment att) {
137            long attKey = findPriorityKey(att.mId);
138            long priorityKey = getPriorityKey(att);
139            if (priorityKey == 0) {
140                if (Email.DEBUG) {
141                    Log.d(TAG, "== Attachment changed: " + att.mId);
142                }
143                // In this case, there is no download priority for this attachment
144                if (attKey != 0) {
145                    // If it exists in the map, remove it and try to stop any download in progress
146                    // NOTE: We don't yet support deleting downloads in progress
147                    if (Email.DEBUG) {
148                        Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
149                    }
150                    remove(attKey, true);
151                }
152            } else {
153                // Ignore changes that occur during download
154                if (mDownloadsInProgress.containsKey(att.mId)) return;
155                // Remove from the map, but don't stop a download in progress
156                DownloadRequest req = remove(attKey, false);
157                if (req == null) {
158                    req = new DownloadRequest(mContext, att);
159                }
160                // If the request already existed, we'll update the priority (so that the time is
161                // up-to-date); otherwise, we create a new request
162                if (Email.DEBUG) {
163                    Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
164                            priorityClass(priorityKey) + ", priority time " +
165                            priorityTime(priorityKey));
166                }
167                // Store the request away
168                put(priorityKey, req);
169            }
170            // Process the queue if we're in a wait
171            kick();
172        }
173
174        /**
175         * Remove a DownloadRequest from the queue, given its priority key
176         * @param priorityKey the priority key to remove (a Long)
177         * @param stopDownload whether we should try to stop an in-progress download of the
178         * attachment with this priority key (not implemented)
179         * @return the DownloadRequest for this priority key (or null, if none)
180         */
181        public synchronized DownloadRequest remove(Object priorityKey, boolean stopDownload) {
182            DownloadRequest req = remove(priorityKey);
183            if ((req != null) && req.inProgress && stopDownload) {
184                // TODO: Stop download
185                if (Email.DEBUG) {
186                    Log.d(TAG, "== Download of " + req.attachmentId + " stopped; NOT implemented");
187                }
188            }
189            return req;
190        }
191
192        /**
193         * Find the priority key of a queued DownloadRequest, given the attachment's id
194         * @param id the id of the attachment
195         * @return the priority key for that attachment (or zero, if none)
196         */
197        private synchronized long findPriorityKey(long id) {
198            for (Entry<Long, DownloadRequest> entry: entrySet()) {
199                if (entry.getValue().attachmentId == id) {
200                    return entry.getKey();
201                }
202            }
203            return 0;
204        }
205
206        /**
207         * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
208         * the limit on maximum downloads
209         */
210        private synchronized void processQueue() {
211            if (Email.DEBUG) {
212                Log.d(TAG, "== Checking attachment queue, " + mAttachmentMap.size() + " entries");
213            }
214            Entry<Long, DownloadRequest> entry = mAttachmentMap.lastEntry();
215            // First, start up any required downloads, in priority order
216            while ((entry != null) && (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
217                DownloadRequest req = entry.getValue();
218                if (!req.inProgress) {
219                    mAttachmentMap.tryStartDownload(req);
220                }
221                entry = mAttachmentMap.lowerEntry(entry.getKey());
222            }
223            // Then, try opportunistic download of appropriate attachments
224            int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
225            if (backgroundDownloads > 0) {
226                // TODO Code for background downloads here
227                if (Email.DEBUG) {
228                    Log.d(TAG, "== We'd look for up to " + backgroundDownloads +
229                            " background download(s) now...");
230                }
231            }
232        }
233
234        /**
235         * Count the number of running downloads in progress for this account
236         * @param accountId the id of the account
237         * @return the count of running downloads
238         */
239        private int downloadsForAccount(long accountId) {
240            int count = 0;
241            for (DownloadRequest req: mDownloadsInProgress.values()) {
242                if (req.accountId == accountId) {
243                    count++;
244                }
245            }
246            return count;
247        }
248
249        /**
250         * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
251         * parameter
252         * @param req the DownloadRequest
253         * @return whether or not the download was started
254         */
255        private synchronized boolean tryStartDownload(DownloadRequest req) {
256            // Enforce per-account limit
257            if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
258                if (Email.DEBUG) {
259                    Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
260                            req.accountId);
261                }
262                return false;
263            }
264            Class<? extends Service> serviceClass = getServiceClassForAccount(req.accountId);
265            if (serviceClass == null) return false;
266            EmailServiceProxy proxy =
267                new EmailServiceProxy(mContext, serviceClass, mServiceCallback);
268            try {
269                File file = AttachmentProvider.getAttachmentFilename(mContext, req.accountId,
270                        req.attachmentId);
271                if (Email.DEBUG) {
272                    Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
273                }
274                proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(),
275                        AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId)
276                            .toString());
277                mDownloadsInProgress.put(req.attachmentId, req);
278                req.inProgress = true;
279            } catch (RemoteException e) {
280                // TODO: Consider whether we need to do more in this case...
281                // For now, fix up our data to reflect the failure
282                mDownloadsInProgress.remove(req.attachmentId);
283                req.inProgress = false;
284            }
285            return true;
286        }
287
288        /**
289         * Called when a download is finished; we get notified of this via our EmailServiceCallback
290         * @param attachmentId the id of the attachment whose download is finished
291         * @param statusCode the EmailServiceStatus code returned by the Service
292         */
293        private synchronized void endDownload(long attachmentId, int statusCode) {
294            // Say we're no longer downloading this
295            mDownloadsInProgress.remove(attachmentId);
296            long priorityKey = mAttachmentMap.findPriorityKey(attachmentId);
297            if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
298                // If this needs to be retried, just process the queue again
299                if (Email.DEBUG) {
300                    Log.d(TAG, "== The download for attachment #" + attachmentId +
301                            " will be retried");
302                }
303                DownloadRequest req = mAttachmentMap.get(priorityKey);
304                if (req != null) {
305                    req.inProgress = false;
306                }
307                kick();
308                return;
309            }
310            DownloadRequest req = mAttachmentMap.remove(priorityKey);
311            if (Email.DEBUG) {
312                long secs = (priorityTime(System.currentTimeMillis()) -
313                        priorityKeyToSystemTime(priorityKey)) / 1000;
314                String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
315                    "Error " + statusCode;
316                Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs +
317                           " seconds from request, status: " + status);
318            }
319            Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
320            if (attachment != null) {
321                boolean deleted = false;
322                if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
323                    if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
324                        // If this is a forwarding download, and the attachment doesn't exist (or
325                        // can't be downloaded) delete it from the outgoing message, lest that
326                        // message never get sent
327                        EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
328                        // TODO: Talk to UX about whether this is even worth doing
329                        showDownloadForwardFailedNotification(attachment);
330                        deleted = true;
331                    }
332                    // If we're an attachment on forwarded mail, and if we're not still blocked,
333                    // try to send pending mail now (as mediated by MailService)
334                    if (!Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
335                        if (Email.DEBUG) {
336                            Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId);
337                        }
338                        MailService.actionSendPendingMail(mContext, req.accountId);
339                    }
340                }
341                if (!deleted) {
342                    // Clear the download flags, since we're done for now.  Note that this happens
343                    // only for non-recoverable errors.  When these occur for forwarded mail, we can
344                    // ignore it and continue; otherwise, it was either 1) a user request, in which
345                    // case the user can retry manually or 2) an opportunistic download, in which
346                    // case the download wasn't critical
347                    ContentValues cv = new ContentValues();
348                    int flags =
349                        Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
350                    cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags);
351                    attachment.update(mContext, cv);
352                }
353            }
354            // Process the queue
355            kick();
356        }
357    }
358
359    private static int systemTimeToPriorityTime(long systemTime) {
360        return Integer.MAX_VALUE - priorityTime(systemTime);
361    }
362
363    private static long priorityKeyToSystemTime(long priorityKey) {
364        return Integer.MAX_VALUE - priorityTime(priorityKey);
365    }
366
367    private static int priorityTime(long priorityKeyOrSystemTime) {
368        return (int)(priorityKeyOrSystemTime & 0xFFFFFFFF);
369    }
370
371    private static long priorityClass(long priorityKey) {
372        return priorityKey >> 32;
373    }
374
375    private static long priorityKey(long priorityClass, long timeInMillis) {
376        // High 32 bits = priority class
377        // Low 32 bits = priority time
378        return (priorityClass<<32) | systemTimeToPriorityTime(timeInMillis);
379    }
380
381    private void kick() {
382        synchronized(mLock) {
383            mLock.notify();
384        }
385    }
386
387    /**
388     * The AttachmentReceiver handles broadcasts from EmailProvider when an attachment is inserted
389     * or updated.  Assuming that the attachment still exists, we just call the AttachmentMap's
390     * onChange() method
391     */
392    public class AttachmentReceiver extends BroadcastReceiver {
393        @Override
394        public void onReceive(Context context, Intent intent) {
395            final Uri attachmentUri = intent.getData();
396            String action = intent.getAction();
397            if (action.equals(EmailProvider.ACTION_ATTACHMENT_UPDATED)) {
398                // We need to look at the flags and see if loading is appropriate
399                final int flags =
400                    intent.getIntExtra(EmailProvider.ATTACHMENT_UPDATED_EXTRA_FLAGS, -1);
401                // If the flags didn't change, we're done
402                if (flags == -1) return;
403                new Thread(new Runnable() {
404                    public void run() {
405                        long id = Long.parseLong(attachmentUri.getLastPathSegment());
406                        Attachment attachment =
407                            Attachment.restoreAttachmentWithId(AttachmentDownloadService.this, id);
408                        if (attachment != null) {
409                            // Store the flags we got from EmailProvider; given that all of this
410                            // activity is asynchronous, we need to use the newest data from
411                            // EmailProvider
412                            attachment.mFlags = flags;
413                            mAttachmentMap.onChange(attachment);
414                        }
415                    }}).run();
416            }
417        }
418    }
419
420    /**
421     * We use an EmailServiceCallback to keep track of the progress of downloads.  These callbacks
422     * come from either Controller (IMAP) or SyncManager (EAS).  Note that we only implement the
423     * single callback that's defined by the EmailServiceCallback interface.
424     */
425    private class ServiceCallback extends IEmailServiceCallback.Stub {
426        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
427                int progress) {
428            if (Email.DEBUG) {
429                String code;
430                switch(statusCode) {
431                    case EmailServiceStatus.SUCCESS:
432                        code = "Success";
433                        break;
434                    case EmailServiceStatus.IN_PROGRESS:
435                        code = "In progress";
436                        break;
437                    default:
438                        code = Integer.toString(statusCode);
439                }
440                Log.d(TAG, "loadAttachmentStatus, id = " + attachmentId + " code = "+ code +
441                        ", " + progress + "%");
442            }
443            // The only thing we're interested in here is whether the download is finished and, if
444            // so, what the result was.
445            switch (statusCode) {
446                case EmailServiceStatus.IN_PROGRESS:
447                    break;
448                default:
449                    mAttachmentMap.endDownload(attachmentId, statusCode);
450                    break;
451            }
452        }
453
454        @Override
455        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
456                throws RemoteException {
457        }
458
459        @Override
460        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
461                throws RemoteException {
462        }
463
464        @Override
465        public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
466                throws RemoteException {
467        }
468    }
469
470    /**
471     * Alert the user that an attachment couldn't be forwarded.  This is a very unusual case, and
472     * perhaps we shouldn't even send a notification. For now, it's helpful for debugging.
473     * Note the STOPSHIP below...
474     */
475    void showDownloadForwardFailedNotification(Attachment att) {
476        // STOPSHIP: Tentative UI; if we use a notification, replace this text with a resource
477        RemoteViews contentView = new RemoteViews(getPackageName(),
478                R.layout.attachment_forward_failed_notification);
479        contentView.setImageViewResource(R.id.image, R.drawable.ic_email_attachment);
480        contentView.setTextViewText(R.id.text,
481                getString(R.string.forward_download_failed_notification, att.mFileName));
482        Notification n = new Notification(R.drawable.stat_notify_email_generic,
483                getString(R.string.forward_download_failed_ticker), System.currentTimeMillis());
484        n.contentView = contentView;
485        Intent i = new Intent(mContext, Welcome.class);
486        PendingIntent pending = PendingIntent.getActivity(mContext, 0, i,
487                PendingIntent.FLAG_UPDATE_CURRENT);
488        n.contentIntent = pending;
489        n.flags = Notification.FLAG_AUTO_CANCEL;
490        NotificationManager nm =
491                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
492        nm.notify(MailService.NOTIFICATION_ID_WARNING, n);
493    }
494
495    /**
496     * Return the class of the service used by the account type of the provided account id.  We
497     * cache the results to avoid repeated database access
498     * @param accountId the id of the account
499     * @return the service class for the account
500     */
501    private synchronized Class<? extends Service> getServiceClassForAccount(long accountId) {
502        // TODO: We should have some more data-driven way of determining the service class. I'd
503        // suggest adding an attribute in the stores.xml file
504        Class<? extends Service> serviceClass = mAccountServiceMap.get(accountId);
505        if (serviceClass == null) {
506            String protocol = Account.getProtocol(mContext, accountId);
507            if (protocol.equals("eas")) {
508                serviceClass = SyncManager.class;
509            } else {
510                serviceClass = com.android.email.Controller.class;
511            }
512            mAccountServiceMap.put(accountId, serviceClass);
513        }
514        return serviceClass;
515    }
516
517    private void onChange(Attachment att) {
518        mAttachmentMap.onChange(att);
519    }
520
521    private boolean isQueued(long attachmentId) {
522        return mAttachmentMap.findPriorityKey(attachmentId) != 0;
523    }
524
525    private boolean dequeue(long attachmentId) {
526        long priority = mAttachmentMap.findPriorityKey(attachmentId);
527        if (priority == 0) return false;
528        DownloadRequest req = mAttachmentMap.remove(priority);
529        return req != null;
530    }
531
532    /**
533     * Ask the service whether a particular attachment is queued for download
534     * @param attachmentId the id of the Attachment (as stored by EmailProvider)
535     * @return whether or not the attachment is queued for download
536     */
537    public static boolean isAttachmentQueued(long attachmentId) {
538        if (sRunningService != null) {
539            return sRunningService.isQueued(attachmentId);
540        }
541        return false;
542    }
543
544    /**
545     * Ask the service to remove an attachment from the download queue
546     * @param attachmentId the id of the Attachment (as stored by EmailProvider)
547     * @return whether or not the attachment was removed from the queue
548     */
549    public static boolean cancelQueuedAttachment(long attachmentId) {
550        if (sRunningService != null) {
551            return sRunningService.dequeue(attachmentId);
552        }
553        return false;
554    }
555
556    /**
557     * Called directly by EmailProvider whenever an attachment is inserted or changed
558     * @param id the attachment's id
559     * @param flags the new flags for the attachment
560     */
561    public static void attachmentChanged(final long id, final int flags) {
562        Utility.runAsync(new Runnable() {
563            public void run() {
564                final Attachment attachment =
565                    Attachment.restoreAttachmentWithId(sRunningService, id);
566                if (attachment != null) {
567                    // Store the flags we got from EmailProvider; given that all of this
568                    // activity is asynchronous, we need to use the newest data from
569                    // EmailProvider
570                    attachment.mFlags = flags;
571                    sRunningService.onChange(attachment);
572                }
573            }});
574    }
575
576    public void run() {
577        mContext = this;
578        // Set up our receiver for EmailProvider attachment updates
579        mAttachmentReceiver = new AttachmentReceiver();
580        try {
581            getApplicationContext().registerReceiver(mAttachmentReceiver,
582                    new IntentFilter(EmailProvider.ACTION_ATTACHMENT_UPDATED,
583                            EmailProvider.EMAIL_ATTACHMENT_MIME_TYPE));
584        } catch (MalformedMimeTypeException e1) {
585            // Since we're passing in a constant mime type, this can't happen
586        }
587
588        // Run through all attachments in the database that require download and add them to
589        // the queue
590        int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
591        Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
592                EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0",
593                new String[] {Integer.toString(mask)}, null);
594        try {
595            Log.d(TAG, "Count: " + c.getCount());
596            while (c.moveToNext()) {
597                Attachment attachment = Attachment.restoreAttachmentWithId(
598                        this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
599                if (attachment != null) {
600                    mAttachmentMap.onChange(attachment);
601                }
602            }
603        } catch (Exception e) {
604            e.printStackTrace();
605        }
606        finally {
607            c.close();
608        }
609
610        // Loop until stopped, with a 30 minute wait loop
611        while (!mStop) {
612            // Here's where we run our attachment loading logic...
613            mAttachmentMap.processQueue();
614            synchronized(mLock) {
615                try {
616                    mLock.wait(PROCESS_QUEUE_WAIT_TIME);
617                } catch (InterruptedException e) {
618                    // That's ok; we'll just keep looping
619                }
620            }
621        }
622    }
623
624    @Override
625    public int onStartCommand(Intent intent, int flags, int startId) {
626        sRunningService = this;
627        return Service.START_STICKY;
628    }
629
630    /**
631     * The lifecycle of this service is managed by Email.setServicesEnabled(), which is called
632     * throughout the code, in particular 1) after boot and 2) after accounts are added or removed
633     * The goal is that this service should be running at all times when there's at least one
634     * email account present.
635     */
636    @Override
637    public void onCreate() {
638        // Start up our service thread
639        new Thread(this, "AttachmentDownloadService").start();
640    }
641    @Override
642    public IBinder onBind(Intent intent) {
643        return null;
644    }
645
646    @Override
647    public void onDestroy() {
648        Log.d(TAG, "**** ON DESTROY!");
649        if (sRunningService != null) {
650            mStop = true;
651            kick();
652        }
653        sRunningService = null;
654    }
655}
656