Controller.java revision 0d1078363581db8caded06cf94e729e88a88761a
1/*
2 * Copyright (C) 2009 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;
18
19import com.android.email.mail.MessagingException;
20import com.android.email.mail.Store;
21import com.android.email.provider.AttachmentProvider;
22import com.android.email.provider.EmailContent;
23import com.android.email.provider.EmailContent.Account;
24import com.android.email.provider.EmailContent.Attachment;
25import com.android.email.provider.EmailContent.Mailbox;
26import com.android.email.provider.EmailContent.Message;
27import com.android.email.service.EmailServiceProxy;
28import com.android.exchange.EmailServiceStatus;
29import com.android.exchange.IEmailService;
30import com.android.exchange.IEmailServiceCallback;
31import com.android.exchange.SyncManager;
32
33import android.content.ContentResolver;
34import android.content.ContentUris;
35import android.content.ContentValues;
36import android.content.Context;
37import android.database.Cursor;
38import android.net.Uri;
39import android.os.RemoteException;
40import android.util.Log;
41
42import java.io.File;
43import java.util.HashSet;
44
45/**
46 * New central controller/dispatcher for Email activities that may require remote operations.
47 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
48 * based code.
49 */
50public class Controller {
51
52    static Controller sInstance;
53    private Context mContext;
54    private Context mProviderContext;
55    private MessagingController mLegacyController;
56    private LegacyListener mLegacyListener = new LegacyListener();
57    private ServiceCallback mServiceCallback = new ServiceCallback();
58    private HashSet<Result> mListeners = new HashSet<Result>();
59
60    private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
61        EmailContent.RECORD_ID,
62        EmailContent.MessageColumns.ACCOUNT_KEY
63    };
64    private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
65
66    protected Controller(Context _context) {
67        mContext = _context;
68        mProviderContext = _context;
69        mLegacyController = MessagingController.getInstance(mContext);
70        mLegacyController.addListener(mLegacyListener);
71    }
72
73    /**
74     * Gets or creates the singleton instance of Controller.
75     * @param _context The context that will be used for all underlying system access
76     */
77    public synchronized static Controller getInstance(Context _context) {
78        if (sInstance == null) {
79            sInstance = new Controller(_context);
80        }
81        return sInstance;
82    }
83
84    /**
85     * For testing only:  Inject a different context for provider access.  This will be
86     * used internally for access the underlying provider (e.g. getContentResolver().query()).
87     * @param providerContext the provider context to be used by this instance
88     */
89    public void setProviderContext(Context providerContext) {
90        mProviderContext = providerContext;
91    }
92
93    /**
94     * Any UI code that wishes for callback results (on async ops) should register their callback
95     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
96     * problems when the command completes and the activity has already paused or finished.
97     * @param listener The callback that may be used in action methods
98     */
99    public void addResultCallback(Result listener) {
100        synchronized (mListeners) {
101            mListeners.add(listener);
102        }
103    }
104
105    /**
106     * Any UI code that no longer wishes for callback results (on async ops) should unregister
107     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
108     * to prevent problems when the command completes and the activity has already paused or
109     * finished.
110     * @param listener The callback that may no longer be used
111     */
112    public void removeResultCallback(Result listener) {
113        synchronized (mListeners) {
114            mListeners.remove(listener);
115        }
116    }
117
118    private boolean isActiveResultCallback(Result listener) {
119        synchronized (mListeners) {
120            return mListeners.contains(listener);
121        }
122    }
123
124    /**
125     * Enable/disable logging for external sync services
126     *
127     * Generally this should be called by anybody who changes Email.DEBUG
128     */
129    public void serviceLogging(int debugEnabled) {
130        IEmailService service =
131            new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback);
132        try {
133            service.setLogging(debugEnabled);
134        } catch (RemoteException e) {
135            // TODO Change exception handling to be consistent with however this method
136            // is implemented for other protocols
137            Log.d("updateMailboxList", "RemoteException" + e);
138        }
139    }
140
141    /**
142     * Request a remote update of mailboxes for an account.
143     *
144     * TODO: Clean up threading in MessagingController cases (or perhaps here in Controller)
145     */
146    public void updateMailboxList(final long accountId, final Result callback) {
147
148        IEmailService service = getServiceForAccount(accountId);
149        if (service != null) {
150            // Service implementation
151            try {
152                service.updateFolderList(accountId);
153            } catch (RemoteException e) {
154                // TODO Change exception handling to be consistent with however this method
155                // is implemented for other protocols
156                Log.d("updateMailboxList", "RemoteException" + e);
157            }
158        } else {
159            // MessagingController implementation
160            new Thread() {
161                @Override
162                public void run() {
163                    mLegacyController.listFolders(accountId, mLegacyListener);
164                }
165            }.start();
166        }
167    }
168
169    /**
170     * Request a remote update of a mailbox.  For use by the timed service.
171     *
172     * Functionally this is quite similar to updateMailbox(), but it's a separate API and
173     * separate callback in order to keep UI callbacks from affecting the service loop.
174     */
175    public void serviceCheckMail(final long accountId, final long mailboxId, final long tag,
176            final Result callback) {
177        IEmailService service = getServiceForAccount(accountId);
178        if (service != null) {
179            // Service implementation
180//            try {
181                // TODO this isn't quite going to work, because we're going to get the
182                // generic (UI) callbacks and not the ones we need to restart the ol' service.
183                // service.startSync(mailboxId, tag);
184                callback.serviceCheckMailCallback(null, accountId, mailboxId, 100, tag);
185//            } catch (RemoteException e) {
186                // TODO Change exception handling to be consistent with however this method
187                // is implemented for other protocols
188//                Log.d("updateMailbox", "RemoteException" + e);
189//            }
190        } else {
191            // MessagingController implementation
192            new Thread() {
193                @Override
194                public void run() {
195                    mLegacyController.checkMail(accountId, tag, mLegacyListener);
196                }
197            }.start();
198        }
199    }
200
201    /**
202     * Request a remote update of a mailbox.
203     *
204     * The contract here should be to try and update the headers ASAP, in order to populate
205     * a simple message list.  We should also at this point queue up a background task of
206     * downloading some/all of the messages in this mailbox, but that should be interruptable.
207     */
208    public void updateMailbox(final long accountId, final long mailboxId, final Result callback) {
209
210        IEmailService service = getServiceForAccount(accountId);
211        if (service != null) {
212            // Service implementation
213            try {
214                service.startSync(mailboxId);
215            } catch (RemoteException e) {
216                // TODO Change exception handling to be consistent with however this method
217                // is implemented for other protocols
218                Log.d("updateMailbox", "RemoteException" + e);
219            }
220        } else {
221            // MessagingController implementation
222            new Thread() {
223                @Override
224                public void run() {
225                    // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
226                    Account account =
227                        EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
228                    Mailbox mailbox =
229                        EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
230                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
231                }
232            }.start();
233        }
234    }
235
236    /**
237     * Saves the message to a mailbox of given type.
238     * This is a synchronous operation taking place in the same thread as the caller.
239     * Upon return the message.mId is set.
240     * @param message the message (must have the mAccountId set).
241     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
242     */
243    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
244        long accountId = message.mAccountKey;
245        long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
246        message.mMailboxKey = mailboxId;
247        message.save(mContext);
248    }
249
250    /**
251     * @param accountId the account id
252     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
253     * @return the id of the mailbox. The mailbox is created if not existing.
254     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
255     * Does not validate the input in other ways (e.g. does not verify the existence of account).
256     */
257    public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
258        if (accountId < 0 || mailboxType < 0) {
259            return Mailbox.NO_MAILBOX;
260        }
261        long mailboxId =
262            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
263        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
264    }
265
266    /**
267     * @param mailboxType the mailbox type
268     * @return the resource string corresponding to the mailbox type, empty if not found.
269     */
270    /* package */ String getSpecialMailboxDisplayName(int mailboxType) {
271        int resId = -1;
272        switch (mailboxType) {
273            case Mailbox.TYPE_INBOX:
274                // TODO: there is no special_mailbox_display_name_inbox; why?
275                resId = R.string.special_mailbox_name_inbox;
276                break;
277            case Mailbox.TYPE_OUTBOX:
278                resId = R.string.special_mailbox_display_name_outbox;
279                break;
280            case Mailbox.TYPE_DRAFTS:
281                resId = R.string.special_mailbox_display_name_drafts;
282                break;
283            case Mailbox.TYPE_TRASH:
284                resId = R.string.special_mailbox_display_name_trash;
285                break;
286            case Mailbox.TYPE_SENT:
287                resId = R.string.special_mailbox_display_name_sent;
288                break;
289        }
290        return resId != -1 ? mContext.getString(resId) : "";
291    }
292
293    /**
294     * Create a mailbox given the account and mailboxType.
295     * TODO: Does this need to be signaled explicitly to the sync engines?
296     * As this method is only used internally ('private'), it does not
297     * validate its inputs (accountId and mailboxType).
298     */
299    /* package */ long createMailbox(long accountId, int mailboxType) {
300        if (accountId < 0 || mailboxType < 0) {
301            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
302            Log.e(Email.LOG_TAG, mes);
303            throw new RuntimeException(mes);
304        }
305        Mailbox box = new Mailbox();
306        box.mAccountKey = accountId;
307        box.mType = mailboxType;
308        box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
309        box.mFlagVisible = true;
310        box.mDisplayName = getSpecialMailboxDisplayName(mailboxType);
311        box.save(mProviderContext);
312        return box.mId;
313    }
314
315    /**
316     * Send a message:
317     * - move the message to Outbox (the message is assumed to be in Drafts).
318     * - perform any necessary notification
319     * @param messageId the id of the message to send
320     */
321    public void sendMessage(long messageId, long accountId) {
322        ContentResolver resolver = mProviderContext.getContentResolver();
323        if (accountId == -1) {
324            accountId = lookupAccountForMessage(messageId);
325        }
326        if (accountId == -1) {
327            // probably the message was not found
328            if (Email.LOGD) {
329                Email.log("no account found for message " + messageId);
330            }
331            return;
332        }
333
334        // Move to Outbox
335        long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
336        ContentValues cv = new ContentValues();
337        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
338
339        // does this need to be SYNCED_CONTENT_URI instead?
340        Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
341        resolver.update(uri, cv, null, null);
342
343        // TODO: notifications
344    }
345
346    /**
347     * @param messageId the id of message
348     * @return the accountId corresponding to the given messageId, or -1 if not found.
349     */
350    private long lookupAccountForMessage(long messageId) {
351        ContentResolver resolver = mProviderContext.getContentResolver();
352        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
353                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
354                                  new String[] { Long.toString(messageId) }, null);
355        try {
356            return c.moveToFirst()
357                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
358                : -1;
359        } finally {
360            c.close();
361        }
362    }
363
364    /**
365     * Delete a single message by moving it to the trash.
366     *
367     * This function has no callback, no result reporting, because the desired outcome
368     * is reflected entirely by changes to one or more cursors.
369     *
370     * @param messageId The id of the message to "delete".
371     * @param accountId The id of the message's account, or -1 if not known by caller
372     *
373     * TODO: Move out of UI thread
374     * TODO: "get account a for message m" should be a utility
375     * TODO: "get mailbox of type n for account a" should be a utility
376     */
377    public void deleteMessage(long messageId, long accountId) {
378        ContentResolver resolver = mProviderContext.getContentResolver();
379
380        // 1.  Look up acct# for message we're deleting
381        Cursor c = null;
382        if (accountId == -1) {
383            accountId = lookupAccountForMessage(messageId);
384        }
385        if (accountId == -1) {
386            return;
387        }
388
389        // 2. Confirm that there is a trash mailbox available
390        // 3.  If there's no trash mailbox, create one
391        // TODO: Does this need to be signaled explicitly to the sync engines?
392        long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
393
394        // 4.  Change the mailbox key for the message we're "deleting"
395        ContentValues cv = new ContentValues();
396        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
397        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
398        resolver.update(uri, cv, null, null);
399
400        // 5.  Drop non-essential data for the message (e.g. attachments)
401        // TODO: find the actual files (if any, if loaded) & delete them
402        c = null;
403        try {
404            c = resolver.query(EmailContent.Attachment.CONTENT_URI,
405                    EmailContent.Attachment.CONTENT_PROJECTION,
406                    EmailContent.AttachmentColumns.MESSAGE_KEY + "=?",
407                    new String[] { Long.toString(messageId) }, null);
408            while (c.moveToNext()) {
409                // delete any associated storage
410                // delete row?
411            }
412        } finally {
413            if (c != null) c.close();
414        }
415
416        // 6.  For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings)
417        // TODO write this
418    }
419
420    /**
421     * Set/clear the unread status of a message
422     *
423     * @param messageId the message to update
424     * @param isRead the new value for the isRead flag
425     */
426    public void setMessageRead(long messageId, boolean isRead) {
427        // TODO this should not be in this thread. queue it up.
428        // TODO Also, it needs to update the read/unread count in the mailbox
429        // TODO kick off service/messagingcontroller actions
430
431        ContentValues cv = new ContentValues();
432        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
433        Uri uri = ContentUris.withAppendedId(
434                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
435        mProviderContext.getContentResolver().update(uri, cv, null, null);
436    }
437
438    /**
439     * Set/clear the favorite status of a message
440     *
441     * @param messageId the message to update
442     * @param isFavorite the new value for the isFavorite flag
443     */
444    public void setMessageFavorite(long messageId, boolean isFavorite) {
445        // TODO this should not be in this thread. queue it up.
446        // TODO kick off service/messagingcontroller actions
447
448        ContentValues cv = new ContentValues();
449        cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
450        Uri uri = ContentUris.withAppendedId(
451                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
452        mProviderContext.getContentResolver().update(uri, cv, null, null);
453    }
454
455    /**
456     * Request that an attachment be loaded.  It will be stored at a location controlled
457     * by the AttachmentProvider.
458     *
459     * @param attachmentId the attachment to load
460     * @param messageId the owner message
461     * @param mailboxId the owner mailbox
462     * @param accountId the owner account
463     * @param callback the Controller callback by which results will be reported
464     */
465    public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
466            final long accountId, final Result callback) {
467
468        File saveToFile = AttachmentProvider.getAttachmentFilename(mContext,
469                accountId, attachmentId);
470        if (saveToFile.exists()) {
471            // The attachment has already been downloaded, so we will just "pretend" to download it
472            synchronized (mListeners) {
473                for (Result listener : mListeners) {
474                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
475                }
476                for (Result listener : mListeners) {
477                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
478                }
479            }
480            return;
481        }
482
483        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
484
485        // Split here for target type (Service or MessagingController)
486        IEmailService service = getServiceForMessage(messageId);
487        if (service != null) {
488            // Service implementation
489            try {
490                service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
491                        AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
492            } catch (RemoteException e) {
493                // TODO Change exception handling to be consistent with however this method
494                // is implemented for other protocols
495                Log.e("onDownloadAttachment", "RemoteException", e);
496            }
497        } else {
498            // MessagingController implementation
499            new Thread() {
500                @Override
501                public void run() {
502                    mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
503                            mLegacyListener);
504                }
505            }.start();
506        }
507    }
508
509    /**
510     * For a given message id, return a service proxy if applicable, or null.
511     *
512     * @param messageId the message of interest
513     * @result service proxy, or null if n/a
514     */
515    private IEmailService getServiceForMessage(long messageId) {
516        // TODO make this more efficient, caching the account, smaller lookup here, etc.
517        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
518        return getServiceForAccount(message.mAccountKey);
519    }
520
521    /**
522     * For a given account id, return a service proxy if applicable, or null.
523     *
524     * @param accountId the message of interest
525     * @result service proxy, or null if n/a
526     */
527    private IEmailService getServiceForAccount(long accountId) {
528        // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc.
529        Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
530        if (isMessagingController(account)) {
531            return null;
532        } else {
533            return new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback);
534        }
535    }
536
537    /**
538     * Simple helper to determine if legacy MessagingController should be used
539     *
540     * TODO this should not require a full account, just an accountId
541     * TODO this should use a cache because we'll be doing this a lot
542     */
543    private boolean isMessagingController(EmailContent.Account account) {
544        Store.StoreInfo info =
545            Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext);
546        String scheme = info.mScheme;
547
548        return ("pop3".equals(scheme) || "imap".equals(scheme));
549    }
550
551    /**
552     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
553     * and the result is observed via provider cursors.  The callback will *not* necessarily be
554     * made from the UI thread, so you may need further handlers to safely make UI updates.
555     */
556    public interface Result {
557        /**
558         * Callback for updateMailboxList
559         *
560         * @param result If null, the operation completed without error
561         * @param accountId The account being operated on
562         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
563         */
564        public void updateMailboxListCallback(MessagingException result, long accountId,
565                int progress);
566
567        /**
568         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
569         * it's a separate call used only by UI's, so we can keep things separate.
570         *
571         * @param result If null, the operation completed without error
572         * @param accountId The account being operated on
573         * @param mailboxId The mailbox being operated on
574         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
575         * @param numNewMessages the number of new messages delivered
576         */
577        public void updateMailboxCallback(MessagingException result, long accountId,
578                long mailboxId, int progress, int numNewMessages);
579
580        /**
581         * Callback for loadAttachment
582         *
583         * @param result if null, the attachment completed - if non-null, terminating with failure
584         * @param messageId the message which contains the attachment
585         * @param attachmentId the attachment being loaded
586         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
587         */
588        public void loadAttachmentCallback(MessagingException result, long messageId,
589                long attachmentId, int progress);
590
591        /**
592         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
593         * it's a separate call used only by the automatic checker service, so we can keep
594         * things separate.
595         *
596         * @param result If null, the operation completed without error
597         * @param accountId The account being operated on
598         * @param mailboxId The mailbox being operated on (may be unknown at start)
599         * @param progress 0 for "starting", no updates, 100 for complete
600         * @param tag the same tag that was passed to serviceCheckMail()
601         */
602        public void serviceCheckMailCallback(MessagingException result, long accountId,
603                long mailboxId, int progress, long tag);
604    }
605
606    /**
607     * Support for receiving callbacks from MessagingController and dealing with UI going
608     * out of scope.
609     */
610    private class LegacyListener extends MessagingListener {
611
612        @Override
613        public void listFoldersStarted(EmailContent.Account account) {
614            synchronized (mListeners) {
615                for (Result l : mListeners) {
616                    l.updateMailboxListCallback(null, account.mId, 0);
617                }
618            }
619        }
620
621        @Override
622        public void listFoldersFailed(EmailContent.Account account, String message) {
623            synchronized (mListeners) {
624                for (Result l : mListeners) {
625                    l.updateMailboxListCallback(new MessagingException(message), account.mId, 0);
626                }
627            }
628        }
629
630        @Override
631        public void listFoldersFinished(EmailContent.Account account) {
632            synchronized (mListeners) {
633                for (Result l : mListeners) {
634                    l.updateMailboxListCallback(null, account.mId, 100);
635                }
636            }
637        }
638
639        @Override
640        public void synchronizeMailboxStarted(EmailContent.Account account,
641                EmailContent.Mailbox folder) {
642            synchronized (mListeners) {
643                for (Result l : mListeners) {
644                    l.updateMailboxCallback(null, account.mId, folder.mId, 0, 0);
645                }
646            }
647        }
648
649        @Override
650        public void synchronizeMailboxFinished(EmailContent.Account account,
651                EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) {
652            synchronized (mListeners) {
653                for (Result l : mListeners) {
654                    l.updateMailboxCallback(null, account.mId, folder.mId, 100, numNewMessages);
655                }
656            }
657        }
658
659        @Override
660        public void synchronizeMailboxFailed(EmailContent.Account account,
661                EmailContent.Mailbox folder, Exception e) {
662            MessagingException me;
663            if (e instanceof MessagingException) {
664                me = (MessagingException) e;
665            } else {
666                me = new MessagingException(e.toString());
667            }
668            synchronized (mListeners) {
669                for (Result l : mListeners) {
670                    l.updateMailboxCallback(me, account.mId, folder.mId, 0, 0);
671                }
672            }
673        }
674
675        @Override
676        public void checkMailStarted(Context context, long accountId, long tag) {
677            synchronized (mListeners) {
678                for (Result l : mListeners) {
679                    l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
680                }
681            }
682        }
683
684        @Override
685        public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
686            synchronized (mListeners) {
687                for (Result l : mListeners) {
688                    l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
689                }
690            }
691        }
692
693        @Override
694        public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
695                boolean requiresDownload) {
696            synchronized (mListeners) {
697                for (Result listener : mListeners) {
698                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
699                }
700            }
701        }
702
703        @Override
704        public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
705            synchronized (mListeners) {
706                for (Result listener : mListeners) {
707                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
708                }
709            }
710        }
711
712        @Override
713        public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
714                String reason) {
715            synchronized (mListeners) {
716                for (Result listener : mListeners) {
717                    listener.loadAttachmentCallback(new MessagingException(reason),
718                            messageId, attachmentId, 0);
719                }
720            }
721        }
722    }
723
724    /**
725     * Service callback for service operations
726     */
727    private class ServiceCallback extends IEmailServiceCallback.Stub {
728
729        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
730
731        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
732                int progress) {
733            MessagingException result = null;
734            switch (statusCode) {
735                case EmailServiceStatus.SUCCESS:
736                    progress = 100;
737                    break;
738                case EmailServiceStatus.IN_PROGRESS:
739                    if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
740                        result = new MessagingException(
741                                String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
742                    }
743                    // discard progress reports that look like sentinels
744                    if (progress < 0 || progress >= 100) {
745                        return;
746                    }
747                    break;
748                default:
749                    result = new MessagingException(String.valueOf(statusCode));
750                break;
751            }
752            synchronized (mListeners) {
753                for (Result listener : mListeners) {
754                    listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
755                }
756            }
757        }
758
759        public void sendMessageStatus(long messageId, int statusCode, int progress) {
760            // TODO Auto-generated method stub
761
762        }
763
764        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
765            MessagingException result= null;
766            switch (statusCode) {
767                case EmailServiceStatus.SUCCESS:
768                    progress = 100;
769                    break;
770                case EmailServiceStatus.IN_PROGRESS:
771                    // discard progress reports that look like sentinels
772                    if (progress < 0 || progress >= 100) {
773                        return;
774                    }
775                    break;
776                default:
777                    result = new MessagingException(String.valueOf(statusCode));
778                break;
779            }
780            synchronized(mListeners) {
781                for (Result listener : mListeners) {
782                    listener.updateMailboxListCallback(result, accountId, progress);
783                }
784            }
785        }
786
787        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
788            MessagingException result= null;
789            switch (statusCode) {
790                case EmailServiceStatus.SUCCESS:
791                    progress = 100;
792                    break;
793                case EmailServiceStatus.IN_PROGRESS:
794                    // discard progress reports that look like sentinels
795                    if (progress < 0 || progress >= 100) {
796                        return;
797                    }
798                    break;
799                default:
800                    result = new MessagingException(String.valueOf(statusCode));
801                break;
802            }
803            // TODO where do we get "number of new messages" as well?
804            // TODO should pass this back instead of looking it up here
805            // TODO smaller projection
806            Mailbox mbx = Mailbox.restoreMailboxWithId(mContext, mailboxId);
807            // The mailbox could have disappeared if the server commanded it
808            if (mbx == null) return;
809            long accountId = mbx.mAccountKey;
810            synchronized(mListeners) {
811                for (Result listener : mListeners) {
812                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
813                }
814            }
815        }
816    }
817}
818