Controller.java revision ec15f2356e47d621584cc3fc84c9c02557e0a0df
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.AuthenticationFailedException;
20import com.android.email.mail.MessagingException;
21import com.android.email.mail.Store;
22import com.android.email.mail.store.Pop3Store.Pop3Message;
23import com.android.email.provider.AttachmentProvider;
24import com.android.email.provider.EmailContent;
25import com.android.email.provider.EmailContent.Account;
26import com.android.email.provider.EmailContent.Attachment;
27import com.android.email.provider.EmailContent.Mailbox;
28import com.android.email.provider.EmailContent.MailboxColumns;
29import com.android.email.provider.EmailContent.Message;
30import com.android.email.provider.EmailContent.MessageColumns;
31import com.android.email.service.EmailServiceStatus;
32import com.android.email.service.IEmailService;
33import com.android.email.service.IEmailServiceCallback;
34
35import android.content.ContentResolver;
36import android.content.ContentUris;
37import android.content.ContentValues;
38import android.content.Context;
39import android.database.Cursor;
40import android.net.Uri;
41import android.os.RemoteException;
42import android.util.Log;
43
44import java.io.File;
45import java.io.FileNotFoundException;
46import java.io.IOException;
47import java.io.InputStream;
48import java.util.HashSet;
49import java.util.concurrent.ConcurrentHashMap;
50
51/**
52 * New central controller/dispatcher for Email activities that may require remote operations.
53 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
54 * based code.
55 */
56public class Controller {
57
58    private static Controller sInstance;
59    private final Context mContext;
60    private Context mProviderContext;
61    private final MessagingController mLegacyController;
62    private final LegacyListener mLegacyListener = new LegacyListener();
63    private final ServiceCallback mServiceCallback = new ServiceCallback();
64    private final HashSet<Result> mListeners = new HashSet<Result>();
65    /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap =
66        new ConcurrentHashMap<Long, Boolean>();
67
68
69    // Note that 0 is a syntactically valid account key; however there can never be an account
70    // with id = 0, so attempts to restore the account will return null.  Null values are
71    // handled properly within the code, so this won't cause any issues.
72    private static final long ATTACHMENT_MAILBOX_ACCOUNT_KEY = 0;
73    /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__";
74    /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__";
75    private static final String WHERE_TYPE_ATTACHMENT =
76        MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT;
77    private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?";
78
79    private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
80        EmailContent.RECORD_ID,
81        EmailContent.MessageColumns.ACCOUNT_KEY
82    };
83    private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
84
85    private static String[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] {
86        EmailContent.RECORD_ID,
87        EmailContent.MessageColumns.MAILBOX_KEY
88    };
89    private static int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1;
90
91    protected Controller(Context _context) {
92        mContext = _context.getApplicationContext();
93        mProviderContext = _context;
94        mLegacyController = MessagingController.getInstance(mProviderContext);
95        mLegacyController.addListener(mLegacyListener);
96    }
97
98    /**
99     * Gets or creates the singleton instance of Controller.
100     */
101    public synchronized static Controller getInstance(Context _context) {
102        if (sInstance == null) {
103            sInstance = new Controller(_context);
104        }
105        return sInstance;
106    }
107
108    /**
109     * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
110     *
111     * Tests that use this method MUST clean it up by calling this method again with null.
112     */
113    public synchronized static void injectMockControllerForTest(Controller mockController) {
114        sInstance = mockController;
115    }
116
117    /**
118     * For testing only:  Inject a different context for provider access.  This will be
119     * used internally for access the underlying provider (e.g. getContentResolver().query()).
120     * @param providerContext the provider context to be used by this instance
121     */
122    public void setProviderContext(Context providerContext) {
123        mProviderContext = providerContext;
124    }
125
126    /**
127     * Any UI code that wishes for callback results (on async ops) should register their callback
128     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
129     * problems when the command completes and the activity has already paused or finished.
130     * @param listener The callback that may be used in action methods
131     */
132    public void addResultCallback(Result listener) {
133        synchronized (mListeners) {
134            mListeners.add(listener);
135        }
136    }
137
138    /**
139     * Any UI code that no longer wishes for callback results (on async ops) should unregister
140     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
141     * to prevent problems when the command completes and the activity has already paused or
142     * finished.
143     * @param listener The callback that may no longer be used
144     */
145    public void removeResultCallback(Result listener) {
146        synchronized (mListeners) {
147            mListeners.remove(listener);
148        }
149    }
150
151    private boolean isActiveResultCallback(Result listener) {
152        synchronized (mListeners) {
153            return mListeners.contains(listener);
154        }
155    }
156
157    /**
158     * Delete all Messages that live in the attachment mailbox
159     */
160    public void deleteAttachmentMessages() {
161        // Note: There should only be one attachment mailbox at present
162        ContentResolver resolver = mProviderContext.getContentResolver();
163        Cursor c = null;
164        try {
165            c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
166                    WHERE_TYPE_ATTACHMENT, null, null);
167            while (c.moveToNext()) {
168                long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
169                // Must delete attachments BEFORE messages
170                AttachmentProvider.deleteAllMailboxAttachmentFiles(mProviderContext, 0, mailboxId);
171                resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY,
172                        new String[] {Long.toString(mailboxId)});
173           }
174        } finally {
175            if (c != null) {
176                c.close();
177            }
178        }
179    }
180
181    /**
182     * Returns the attachment Mailbox (where we store eml attachment Emails), creating one
183     * if necessary
184     * @return the account's temporary Mailbox
185     */
186    public Mailbox getAttachmentMailbox() {
187        Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI,
188                Mailbox.CONTENT_PROJECTION, WHERE_TYPE_ATTACHMENT, null, null);
189        try {
190            if (c.moveToFirst()) {
191                return new Mailbox().restore(c);
192            }
193        } finally {
194            c.close();
195        }
196        Mailbox m = new Mailbox();
197        m.mAccountKey = ATTACHMENT_MAILBOX_ACCOUNT_KEY;
198        m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID;
199        m.mFlagVisible = false;
200        m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID;
201        m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
202        m.mType = Mailbox.TYPE_ATTACHMENT;
203        m.save(mProviderContext);
204        return m;
205    }
206
207    /**
208     * Create a Message from the Uri and store it in the attachment mailbox
209     * @param uri the uri containing message content
210     * @return the Message or null
211     */
212    public Message loadMessageFromUri(Uri uri) {
213        Mailbox mailbox = getAttachmentMailbox();
214        if (mailbox == null) return null;
215        try {
216            InputStream is = mProviderContext.getContentResolver().openInputStream(uri);
217            try {
218                // First, create a Pop3Message from the attachment and then parse it
219                Pop3Message pop3Message = new Pop3Message(
220                        ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null);
221                pop3Message.parse(is);
222                // Now, pull out the header fields
223                Message msg = new Message();
224                LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId);
225                // Commit the message to the local store
226                msg.save(mProviderContext);
227                // Setup the rest of the message and mark it completely loaded
228                mLegacyController.copyOneMessageToProvider(pop3Message, msg,
229                        Message.FLAG_LOADED_COMPLETE, mProviderContext);
230                // Restore the complete message and return it
231                return Message.restoreMessageWithId(mProviderContext, msg.mId);
232            } catch (MessagingException e) {
233            } catch (IOException e) {
234            }
235        } catch (FileNotFoundException e) {
236        }
237        return null;
238    }
239
240    /**
241     * Enable/disable logging for external sync services
242     *
243     * Generally this should be called by anybody who changes Email.DEBUG
244     */
245    public void serviceLogging(int debugEnabled) {
246        IEmailService service = ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
247        try {
248            service.setLogging(debugEnabled);
249        } catch (RemoteException e) {
250            // TODO Change exception handling to be consistent with however this method
251            // is implemented for other protocols
252            Log.d("updateMailboxList", "RemoteException" + e);
253        }
254    }
255
256    /**
257     * Request a remote update of mailboxes for an account.
258     */
259    public void updateMailboxList(final long accountId) {
260        Utility.runAsync(new Runnable() {
261            @Override
262            public void run() {
263                final IEmailService service = getServiceForAccount(accountId);
264                if (service != null) {
265                    // Service implementation
266                    try {
267                        service.updateFolderList(accountId);
268                    } catch (RemoteException e) {
269                        // TODO Change exception handling to be consistent with however this method
270                        // is implemented for other protocols
271                        Log.d("updateMailboxList", "RemoteException" + e);
272                    }
273                } else {
274                    // MessagingController implementation
275                    mLegacyController.listFolders(accountId, mLegacyListener);
276                }
277            }
278        });
279    }
280
281    /**
282     * Request a remote update of a mailbox.  For use by the timed service.
283     *
284     * Functionally this is quite similar to updateMailbox(), but it's a separate API and
285     * separate callback in order to keep UI callbacks from affecting the service loop.
286     */
287    public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) {
288        IEmailService service = getServiceForAccount(accountId);
289        if (service != null) {
290            // Service implementation
291//            try {
292                // TODO this isn't quite going to work, because we're going to get the
293                // generic (UI) callbacks and not the ones we need to restart the ol' service.
294                // service.startSync(mailboxId, tag);
295            mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag);
296//            } catch (RemoteException e) {
297                // TODO Change exception handling to be consistent with however this method
298                // is implemented for other protocols
299//                Log.d("updateMailbox", "RemoteException" + e);
300//            }
301        } else {
302            // MessagingController implementation
303            Utility.runAsync(new Runnable() {
304                public void run() {
305                    mLegacyController.checkMail(accountId, tag, mLegacyListener);
306                }
307            });
308        }
309    }
310
311    /**
312     * Request a remote update of a mailbox.
313     *
314     * The contract here should be to try and update the headers ASAP, in order to populate
315     * a simple message list.  We should also at this point queue up a background task of
316     * downloading some/all of the messages in this mailbox, but that should be interruptable.
317     */
318    public void updateMailbox(final long accountId, final long mailboxId) {
319
320        IEmailService service = getServiceForAccount(accountId);
321        if (service != null) {
322            // Service implementation
323            try {
324                service.startSync(mailboxId);
325            } catch (RemoteException e) {
326                // TODO Change exception handling to be consistent with however this method
327                // is implemented for other protocols
328                Log.d("updateMailbox", "RemoteException" + e);
329            }
330        } else {
331            // MessagingController implementation
332            Utility.runAsync(new Runnable() {
333                public void run() {
334                    // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
335                    Account account =
336                        EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
337                    Mailbox mailbox =
338                        EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
339                    if (account == null || mailbox == null) {
340                        return;
341                    }
342                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
343                }
344            });
345        }
346    }
347
348    /**
349     * Request that any final work necessary be done, to load a message.
350     *
351     * Note, this assumes that the caller has already checked message.mFlagLoaded and that
352     * additional work is needed.  There is no optimization here for a message which is already
353     * loaded.
354     *
355     * @param messageId the message to load
356     * @param callback the Controller callback by which results will be reported
357     */
358    public void loadMessageForView(final long messageId) {
359
360        // Split here for target type (Service or MessagingController)
361        IEmailService service = getServiceForMessage(messageId);
362        if (service != null) {
363            // There is no service implementation, so we'll just jam the value, log the error,
364            // and get out of here.
365            Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
366            ContentValues cv = new ContentValues();
367            cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
368            mProviderContext.getContentResolver().update(uri, cv, null, null);
369            Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
370            synchronized (mListeners) {
371                for (Result listener : mListeners) {
372                    listener.loadMessageForViewCallback(null, messageId, 100);
373                }
374            }
375        } else {
376            // MessagingController implementation
377            Utility.runAsync(new Runnable() {
378                public void run() {
379                    mLegacyController.loadMessageForView(messageId, mLegacyListener);
380                }
381            });
382        }
383    }
384
385
386    /**
387     * Saves the message to a mailbox of given type.
388     * This is a synchronous operation taking place in the same thread as the caller.
389     * Upon return the message.mId is set.
390     * @param message the message (must have the mAccountId set).
391     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
392     */
393    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
394        long accountId = message.mAccountKey;
395        long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
396        message.mMailboxKey = mailboxId;
397        message.save(mProviderContext);
398    }
399
400    /**
401     * @param accountId the account id
402     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
403     * @return the id of the mailbox. The mailbox is created if not existing.
404     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
405     * Does not validate the input in other ways (e.g. does not verify the existence of account).
406     */
407    public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
408        if (accountId < 0 || mailboxType < 0) {
409            return Mailbox.NO_MAILBOX;
410        }
411        long mailboxId =
412            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
413        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
414    }
415
416    /**
417     * Returns the server-side name for a specific mailbox.
418     *
419     * @param mailboxType the mailbox type
420     * @return the resource string corresponding to the mailbox type, empty if not found.
421     */
422    /* package */ String getMailboxServerName(int mailboxType) {
423        int resId = -1;
424        switch (mailboxType) {
425            case Mailbox.TYPE_INBOX:
426                resId = R.string.mailbox_name_server_inbox;
427                break;
428            case Mailbox.TYPE_OUTBOX:
429                resId = R.string.mailbox_name_server_outbox;
430                break;
431            case Mailbox.TYPE_DRAFTS:
432                resId = R.string.mailbox_name_server_drafts;
433                break;
434            case Mailbox.TYPE_TRASH:
435                resId = R.string.mailbox_name_server_trash;
436                break;
437            case Mailbox.TYPE_SENT:
438                resId = R.string.mailbox_name_server_sent;
439                break;
440            case Mailbox.TYPE_JUNK:
441                resId = R.string.mailbox_name_server_junk;
442                break;
443        }
444        return resId != -1 ? mContext.getString(resId) : "";
445    }
446
447    /**
448     * Create a mailbox given the account and mailboxType.
449     * TODO: Does this need to be signaled explicitly to the sync engines?
450     */
451    /* package */ long createMailbox(long accountId, int mailboxType) {
452        if (accountId < 0 || mailboxType < 0) {
453            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
454            Log.e(Email.LOG_TAG, mes);
455            throw new RuntimeException(mes);
456        }
457        Mailbox box = new Mailbox();
458        box.mAccountKey = accountId;
459        box.mType = mailboxType;
460        box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
461        box.mFlagVisible = true;
462        box.mDisplayName = getMailboxServerName(mailboxType);
463        box.save(mProviderContext);
464        return box.mId;
465    }
466
467    /**
468     * Send a message:
469     * - move the message to Outbox (the message is assumed to be in Drafts).
470     * - EAS service will take it from there
471     * - trigger send for POP/IMAP
472     * @param messageId the id of the message to send
473     */
474    public void sendMessage(long messageId, long accountId) {
475        ContentResolver resolver = mProviderContext.getContentResolver();
476        if (accountId == -1) {
477            accountId = lookupAccountForMessage(messageId);
478        }
479        if (accountId == -1) {
480            // probably the message was not found
481            if (Email.LOGD) {
482                Email.log("no account found for message " + messageId);
483            }
484            return;
485        }
486
487        // Move to Outbox
488        long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
489        ContentValues cv = new ContentValues();
490        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
491
492        // does this need to be SYNCED_CONTENT_URI instead?
493        Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
494        resolver.update(uri, cv, null, null);
495
496        // Split here for target type (Service or MessagingController)
497        IEmailService service = getServiceForMessage(messageId);
498        if (service != null) {
499            // We just need to be sure the callback is installed, if this is the first call
500            // to the service.
501            try {
502                service.setCallback(mServiceCallback);
503            } catch (RemoteException re) {
504                // OK - not a critical callback here
505            }
506        } else {
507            // for IMAP & POP only, (attempt to) send the message now
508            final EmailContent.Account account =
509                    EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
510            if (account == null) {
511                return;
512            }
513            final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
514            Utility.runAsync(new Runnable() {
515                public void run() {
516                    mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
517                }
518            });
519        }
520    }
521
522    /**
523     * Try to send all pending messages for a given account
524     *
525     * @param accountId the account for which to send messages (-1 for all accounts)
526     * @param callback
527     */
528    public void sendPendingMessages(long accountId) {
529        // 1. make sure we even have an outbox, exit early if not
530        final long outboxId =
531            Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
532        if (outboxId == Mailbox.NO_MAILBOX) {
533            return;
534        }
535
536        // 2. dispatch as necessary
537        IEmailService service = getServiceForAccount(accountId);
538        if (service != null) {
539            // Service implementation
540            try {
541                service.startSync(outboxId);
542            } catch (RemoteException e) {
543                // TODO Change exception handling to be consistent with however this method
544                // is implemented for other protocols
545                Log.d("updateMailbox", "RemoteException" + e);
546            }
547        } else {
548            // MessagingController implementation
549            final EmailContent.Account account =
550                EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
551            if (account == null) {
552                return;
553            }
554            final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
555            Utility.runAsync(new Runnable() {
556                public void run() {
557                    mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
558                }
559            });
560        }
561    }
562
563    /**
564     * Call {@link #sendPendingMessages} for all accounts.
565     */
566    public void sendPendingMessagesForAllAccounts(final Context context) {
567        Utility.runAsync(new Runnable() {
568            public void run() {
569                Cursor c = context.getContentResolver().query(Account.CONTENT_URI,
570                        Account.ID_PROJECTION, null, null, null);
571                try {
572                    while (c.moveToNext()) {
573                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
574                        sendPendingMessages(accountId);
575                    }
576                } finally {
577                    c.close();
578                }
579            }
580        });
581    }
582
583    /**
584     * Reset visible limits for all accounts.
585     * For each account:
586     *   look up limit
587     *   write limit into all mailboxes for that account
588     */
589    public void resetVisibleLimits() {
590        Utility.runAsync(new Runnable() {
591            public void run() {
592                ContentResolver resolver = mProviderContext.getContentResolver();
593                Cursor c = null;
594                try {
595                    c = resolver.query(
596                            Account.CONTENT_URI,
597                            Account.ID_PROJECTION,
598                            null, null, null);
599                    while (c.moveToNext()) {
600                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
601                        Account account = Account.restoreAccountWithId(mProviderContext, accountId);
602                        if (account != null) {
603                            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
604                                    account.getStoreUri(mProviderContext), mContext);
605                            if (info != null && info.mVisibleLimitDefault > 0) {
606                                int limit = info.mVisibleLimitDefault;
607                                ContentValues cv = new ContentValues();
608                                cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
609                                resolver.update(Mailbox.CONTENT_URI, cv,
610                                        MailboxColumns.ACCOUNT_KEY + "=?",
611                                        new String[] { Long.toString(accountId) });
612                            }
613                        }
614                    }
615                } finally {
616                    if (c != null) {
617                        c.close();
618                    }
619                }
620            }
621        });
622    }
623
624    /**
625     * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
626     * IMAP and POP.
627     *
628     * @param mailboxId the mailbox
629     * @param callback
630     */
631    public void loadMoreMessages(final long mailboxId) {
632        Utility.runAsync(new Runnable() {
633            public void run() {
634                Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
635                if (mailbox == null) {
636                    return;
637                }
638                Account account = Account.restoreAccountWithId(mProviderContext,
639                        mailbox.mAccountKey);
640                if (account == null) {
641                    return;
642                }
643                Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
644                        account.getStoreUri(mProviderContext), mContext);
645                if (info != null && info.mVisibleLimitIncrement > 0) {
646                    // Use provider math to increment the field
647                    ContentValues cv = new ContentValues();;
648                    cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
649                    cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement);
650                    Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
651                    mProviderContext.getContentResolver().update(uri, cv, null, null);
652                    // Trigger a refresh using the new, longer limit
653                    mailbox.mVisibleLimit += info.mVisibleLimitIncrement;
654                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
655                }
656            }
657        });
658    }
659
660    /**
661     * @param messageId the id of message
662     * @return the accountId corresponding to the given messageId, or -1 if not found.
663     */
664    private long lookupAccountForMessage(long messageId) {
665        ContentResolver resolver = mProviderContext.getContentResolver();
666        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
667                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
668                                  new String[] { Long.toString(messageId) }, null);
669        try {
670            return c.moveToFirst()
671                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
672                : -1;
673        } finally {
674            c.close();
675        }
676    }
677
678    /**
679     * Delete a single attachment entry from the DB given its id.
680     * Does not delete any eventual associated files.
681     */
682    public void deleteAttachment(long attachmentId) {
683        ContentResolver resolver = mProviderContext.getContentResolver();
684        Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
685        resolver.delete(uri, null, null);
686    }
687
688    /**
689     * Delete a single message by moving it to the trash, or deleting it from the trash
690     *
691     * This function has no callback, no result reporting, because the desired outcome
692     * is reflected entirely by changes to one or more cursors.
693     *
694     * @param messageId The id of the message to "delete".
695     * @param accountId The id of the message's account, or -1 if not known by caller
696     *
697     * TODO: Move out of UI thread
698     * TODO: "get account a for message m" should be a utility
699     * TODO: "get mailbox of type n for account a" should be a utility
700     */
701    public void deleteMessage(long messageId, long accountId) {
702        ContentResolver resolver = mProviderContext.getContentResolver();
703
704        // 1.  Look up acct# for message we're deleting
705        if (accountId == -1) {
706            accountId = lookupAccountForMessage(messageId);
707        }
708        if (accountId == -1) {
709            return;
710        }
711
712        // 2. Confirm that there is a trash mailbox available.  If not, create one
713        long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
714
715        // 3.  Are we moving to trash or deleting?  It depends on where the message currently sits.
716        long sourceMailboxId = -1;
717        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
718                MESSAGEID_TO_MAILBOXID_PROJECTION, EmailContent.RECORD_ID + "=?",
719                new String[] { Long.toString(messageId) }, null);
720        try {
721            sourceMailboxId = c.moveToFirst()
722                ? c.getLong(MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID)
723                : -1;
724        } finally {
725            c.close();
726        }
727
728        // 4.  Drop non-essential data for the message (e.g. attachment files)
729        AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, accountId, messageId);
730
731        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
732
733        // 5. Perform "delete" as appropriate
734        if (sourceMailboxId == trashMailboxId) {
735            // 5a. Delete from trash
736            resolver.delete(uri, null, null);
737        } else {
738            // 5b. Move to trash
739            ContentValues cv = new ContentValues();
740            cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
741            resolver.update(uri, cv, null, null);
742        }
743
744        // 6.  Service runs automatically, MessagingController needs a kick
745        Account account = Account.restoreAccountWithId(mProviderContext, accountId);
746        if (account == null) {
747            return; // isMessagingController returns false for null, but let's make it clear.
748        }
749        if (isMessagingController(account)) {
750            final long syncAccountId = accountId;
751            Utility.runAsync(new Runnable() {
752                public void run() {
753                    mLegacyController.processPendingActions(syncAccountId);
754                }
755            });
756        }
757    }
758
759    /**
760     * Set/clear the unread status of a message
761     *
762     * TODO db ops should not be in this thread. queue it up.
763     *
764     * @param messageId the message to update
765     * @param isRead the new value for the isRead flag
766     */
767    public void setMessageRead(final long messageId, boolean isRead) {
768        ContentValues cv = new ContentValues();
769        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
770        Uri uri = ContentUris.withAppendedId(
771                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
772        mProviderContext.getContentResolver().update(uri, cv, null, null);
773
774        // Service runs automatically, MessagingController needs a kick
775        final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
776        if (message == null) {
777            return;
778        }
779        Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
780        if (account == null) {
781            return; // isMessagingController returns false for null, but let's make it clear.
782        }
783        if (isMessagingController(account)) {
784            Utility.runAsync(new Runnable() {
785                public void run() {
786                    mLegacyController.processPendingActions(message.mAccountKey);
787                }
788            });
789        }
790    }
791
792    /**
793     * Set/clear the favorite status of a message
794     *
795     * TODO db ops should not be in this thread. queue it up.
796     *
797     * @param messageId the message to update
798     * @param isFavorite the new value for the isFavorite flag
799     */
800    public void setMessageFavorite(final long messageId, boolean isFavorite) {
801        ContentValues cv = new ContentValues();
802        cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
803        Uri uri = ContentUris.withAppendedId(
804                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
805        mProviderContext.getContentResolver().update(uri, cv, null, null);
806
807        // Service runs automatically, MessagingController needs a kick
808        final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
809        if (message == null) {
810            return;
811        }
812        Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
813        if (account == null) {
814            return; // isMessagingController returns false for null, but let's make it clear.
815        }
816        if (isMessagingController(account)) {
817            Utility.runAsync(new Runnable() {
818                public void run() {
819                    mLegacyController.processPendingActions(message.mAccountKey);
820                }
821            });
822        }
823    }
824
825    /**
826     * Respond to a meeting invitation.
827     *
828     * @param messageId the id of the invitation being responded to
829     * @param response the code representing the response to the invitation
830     */
831    public void sendMeetingResponse(final long messageId, final int response) {
832         // Split here for target type (Service or MessagingController)
833        IEmailService service = getServiceForMessage(messageId);
834        if (service != null) {
835            // Service implementation
836            try {
837                service.sendMeetingResponse(messageId, response);
838            } catch (RemoteException e) {
839                // TODO Change exception handling to be consistent with however this method
840                // is implemented for other protocols
841                Log.e("onDownloadAttachment", "RemoteException", e);
842            }
843        }
844    }
845
846    /**
847     * Request that an attachment be loaded.  It will be stored at a location controlled
848     * by the AttachmentProvider.
849     *
850     * @param attachmentId the attachment to load
851     * @param messageId the owner message
852     * @param mailboxId the owner mailbox
853     * @param accountId the owner account
854     */
855    public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
856            final long accountId) {
857
858        File saveToFile = AttachmentProvider.getAttachmentFilename(mProviderContext,
859                accountId, attachmentId);
860        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
861
862        if (saveToFile.exists() && attachInfo.mContentUri != null) {
863            // The attachment has already been downloaded, so we will just "pretend" to download it
864            synchronized (mListeners) {
865                for (Result listener : mListeners) {
866                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
867                }
868                for (Result listener : mListeners) {
869                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
870                }
871            }
872            return;
873        }
874
875        // Split here for target type (Service or MessagingController)
876        IEmailService service = getServiceForMessage(messageId);
877        if (service != null) {
878            // Service implementation
879            try {
880                service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
881                        AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
882            } catch (RemoteException e) {
883                // TODO Change exception handling to be consistent with however this method
884                // is implemented for other protocols
885                Log.e("onDownloadAttachment", "RemoteException", e);
886            }
887        } else {
888            // MessagingController implementation
889            Utility.runAsync(new Runnable() {
890                public void run() {
891                    mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
892                            mLegacyListener);
893                }
894            });
895        }
896    }
897
898    /**
899     * For a given message id, return a service proxy if applicable, or null.
900     *
901     * @param messageId the message of interest
902     * @result service proxy, or null if n/a
903     */
904    private IEmailService getServiceForMessage(long messageId) {
905        // TODO make this more efficient, caching the account, smaller lookup here, etc.
906        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
907        if (message == null) {
908            return null;
909        }
910        return getServiceForAccount(message.mAccountKey);
911    }
912
913    /**
914     * For a given account id, return a service proxy if applicable, or null.
915     *
916     * @param accountId the message of interest
917     * @result service proxy, or null if n/a
918     */
919    private IEmailService getServiceForAccount(long accountId) {
920        if (isMessagingController(accountId)) return null;
921        return getExchangeEmailService();
922    }
923
924    private IEmailService getExchangeEmailService() {
925        return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
926    }
927
928    /**
929     * Simple helper to determine if legacy MessagingController should be used
930     */
931    public boolean isMessagingController(EmailContent.Account account) {
932        if (account == null) return false;
933        return isMessagingController(account.mId);
934    }
935
936    public boolean isMessagingController(long accountId) {
937        Boolean isLegacyController = mLegacyControllerMap.get(accountId);
938        if (isLegacyController == null) {
939            String protocol = Account.getProtocol(mProviderContext, accountId);
940            isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol));
941            mLegacyControllerMap.put(accountId, isLegacyController);
942        }
943        return isLegacyController;
944    }
945
946    /**
947     * Delete an account.
948     */
949    public void deleteAccount(final long accountId) {
950        Utility.runAsync(new Runnable() {
951            public void run() {
952                deleteAccountSync(accountId);
953            }
954        });
955    }
956
957    /**
958     * Delete an account synchronously.  Intended to be used only by unit tests.
959     */
960    public void deleteAccountSync(long accountId) {
961        try {
962            mLegacyControllerMap.remove(accountId);
963            // Get the account URI.
964            final Account account = Account.restoreAccountWithId(mContext, accountId);
965            if (account == null) {
966                return; // Already deleted?
967            }
968            final String accountUri = account.getStoreUri(mContext);
969
970            // Delete Remote store at first.
971            Store.getInstance(accountUri, mContext, null).delete();
972
973            // Remove the Store instance from cache.
974            Store.removeInstance(accountUri);
975            Uri uri = ContentUris.withAppendedId(
976                    EmailContent.Account.CONTENT_URI, accountId);
977            mContext.getContentResolver().delete(uri, null, null);
978
979            // Update the backup (side copy) of the accounts
980            AccountBackupRestore.backupAccounts(mContext);
981
982            // Release or relax device administration, if relevant
983            SecurityPolicy.getInstance(mContext).reducePolicies();
984
985            Email.setServicesEnabled(mContext);
986        } catch (Exception e) {
987            // Ignore
988        } finally {
989            synchronized (mListeners) {
990                for (Result l : mListeners) {
991                    l.deleteAccountCallback(accountId);
992                }
993            }
994        }
995    }
996
997    /**
998     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
999     * and the result is observed via provider cursors.  The callback will *not* necessarily be
1000     * made from the UI thread, so you may need further handlers to safely make UI updates.
1001     */
1002    public static abstract class Result {
1003        /**
1004         * Callback for updateMailboxList
1005         *
1006         * @param result If null, the operation completed without error
1007         * @param accountId The account being operated on
1008         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1009         */
1010        public void updateMailboxListCallback(MessagingException result, long accountId,
1011                int progress) {
1012        }
1013
1014        /**
1015         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
1016         * it's a separate call used only by UI's, so we can keep things separate.
1017         *
1018         * @param result If null, the operation completed without error
1019         * @param accountId The account being operated on
1020         * @param mailboxId The mailbox being operated on
1021         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1022         * @param numNewMessages the number of new messages delivered
1023         */
1024        public void updateMailboxCallback(MessagingException result, long accountId,
1025                long mailboxId, int progress, int numNewMessages) {
1026        }
1027
1028        /**
1029         * Callback for loadMessageForView
1030         *
1031         * @param result if null, the attachment completed - if non-null, terminating with failure
1032         * @param messageId the message which contains the attachment
1033         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1034         */
1035        public void loadMessageForViewCallback(MessagingException result, long messageId,
1036                int progress) {
1037        }
1038
1039        /**
1040         * Callback for loadAttachment
1041         *
1042         * @param result if null, the attachment completed - if non-null, terminating with failure
1043         * @param messageId the message which contains the attachment
1044         * @param attachmentId the attachment being loaded
1045         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1046         */
1047        public void loadAttachmentCallback(MessagingException result, long messageId,
1048                long attachmentId, int progress) {
1049        }
1050
1051        /**
1052         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
1053         * it's a separate call used only by the automatic checker service, so we can keep
1054         * things separate.
1055         *
1056         * @param result If null, the operation completed without error
1057         * @param accountId The account being operated on
1058         * @param mailboxId The mailbox being operated on (may be unknown at start)
1059         * @param progress 0 for "starting", no updates, 100 for complete
1060         * @param tag the same tag that was passed to serviceCheckMail()
1061         */
1062        public void serviceCheckMailCallback(MessagingException result, long accountId,
1063                long mailboxId, int progress, long tag) {
1064        }
1065
1066        /**
1067         * Callback for sending pending messages.  This will be called once to start the
1068         * group, multiple times for messages, and once to complete the group.
1069         *
1070         * @param result If null, the operation completed without error
1071         * @param accountId The account being operated on
1072         * @param messageId The being sent (may be unknown at start)
1073         * @param progress 0 for "starting", 100 for complete
1074         */
1075        public void sendMailCallback(MessagingException result, long accountId,
1076                long messageId, int progress) {
1077        }
1078
1079        /**
1080         * Callback from {@link Controller#deleteAccount}.
1081         */
1082        public void deleteAccountCallback(long accountId) {
1083        }
1084    }
1085
1086    /**
1087     * Support for receiving callbacks from MessagingController and dealing with UI going
1088     * out of scope.
1089     */
1090    private class LegacyListener extends MessagingListener {
1091
1092        @Override
1093        public void listFoldersStarted(long accountId) {
1094            synchronized (mListeners) {
1095                for (Result l : mListeners) {
1096                    l.updateMailboxListCallback(null, accountId, 0);
1097                }
1098            }
1099        }
1100
1101        @Override
1102        public void listFoldersFailed(long accountId, String message) {
1103            synchronized (mListeners) {
1104                for (Result l : mListeners) {
1105                    l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
1106                }
1107            }
1108        }
1109
1110        @Override
1111        public void listFoldersFinished(long accountId) {
1112            synchronized (mListeners) {
1113                for (Result l : mListeners) {
1114                    l.updateMailboxListCallback(null, accountId, 100);
1115                }
1116            }
1117        }
1118
1119        @Override
1120        public void synchronizeMailboxStarted(long accountId, long mailboxId) {
1121            synchronized (mListeners) {
1122                for (Result l : mListeners) {
1123                    l.updateMailboxCallback(null, accountId, mailboxId, 0, 0);
1124                }
1125            }
1126        }
1127
1128        @Override
1129        public void synchronizeMailboxFinished(long accountId, long mailboxId,
1130                int totalMessagesInMailbox, int numNewMessages) {
1131            synchronized (mListeners) {
1132                for (Result l : mListeners) {
1133                    l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages);
1134                }
1135            }
1136        }
1137
1138        @Override
1139        public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
1140            MessagingException me;
1141            if (e instanceof MessagingException) {
1142                me = (MessagingException) e;
1143            } else {
1144                me = new MessagingException(e.toString());
1145            }
1146            synchronized (mListeners) {
1147                for (Result l : mListeners) {
1148                    l.updateMailboxCallback(me, accountId, mailboxId, 0, 0);
1149                }
1150            }
1151        }
1152
1153        @Override
1154        public void checkMailStarted(Context context, long accountId, long tag) {
1155            synchronized (mListeners) {
1156                for (Result l : mListeners) {
1157                    l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
1158                }
1159            }
1160        }
1161
1162        @Override
1163        public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
1164            synchronized (mListeners) {
1165                for (Result l : mListeners) {
1166                    l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
1167                }
1168            }
1169        }
1170
1171        @Override
1172        public void loadMessageForViewStarted(long messageId) {
1173            synchronized (mListeners) {
1174                for (Result listener : mListeners) {
1175                    listener.loadMessageForViewCallback(null, messageId, 0);
1176                }
1177            }
1178        }
1179
1180        @Override
1181        public void loadMessageForViewFinished(long messageId) {
1182            synchronized (mListeners) {
1183                for (Result listener : mListeners) {
1184                    listener.loadMessageForViewCallback(null, messageId, 100);
1185                }
1186            }
1187        }
1188
1189        @Override
1190        public void loadMessageForViewFailed(long messageId, String message) {
1191            synchronized (mListeners) {
1192                for (Result listener : mListeners) {
1193                    listener.loadMessageForViewCallback(new MessagingException(message),
1194                            messageId, 0);
1195                }
1196            }
1197        }
1198
1199        @Override
1200        public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
1201                boolean requiresDownload) {
1202            synchronized (mListeners) {
1203                for (Result listener : mListeners) {
1204                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
1205                }
1206            }
1207        }
1208
1209        @Override
1210        public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
1211            synchronized (mListeners) {
1212                for (Result listener : mListeners) {
1213                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
1214                }
1215            }
1216        }
1217
1218        @Override
1219        public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
1220                String reason) {
1221            synchronized (mListeners) {
1222                for (Result listener : mListeners) {
1223                    listener.loadAttachmentCallback(new MessagingException(reason),
1224                            messageId, attachmentId, 0);
1225                }
1226            }
1227        }
1228
1229        @Override
1230        synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
1231            synchronized (mListeners) {
1232                for (Result listener : mListeners) {
1233                    listener.sendMailCallback(null, accountId, messageId, 0);
1234                }
1235            }
1236        }
1237
1238        @Override
1239        synchronized public void sendPendingMessagesCompleted(long accountId) {
1240            synchronized (mListeners) {
1241                for (Result listener : mListeners) {
1242                    listener.sendMailCallback(null, accountId, -1, 100);
1243                }
1244            }
1245        }
1246
1247        @Override
1248        synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
1249                Exception reason) {
1250            MessagingException me;
1251            if (reason instanceof MessagingException) {
1252                me = (MessagingException) reason;
1253            } else {
1254                me = new MessagingException(reason.toString());
1255            }
1256            synchronized (mListeners) {
1257                for (Result listener : mListeners) {
1258                    listener.sendMailCallback(me, accountId, messageId, 0);
1259                }
1260            }
1261        }
1262    }
1263
1264    /**
1265     * Service callback for service operations
1266     */
1267    private class ServiceCallback extends IEmailServiceCallback.Stub {
1268
1269        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1270
1271        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1272                int progress) {
1273            MessagingException result = mapStatusToException(statusCode);
1274            switch (statusCode) {
1275                case EmailServiceStatus.SUCCESS:
1276                    progress = 100;
1277                    break;
1278                case EmailServiceStatus.IN_PROGRESS:
1279                    if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1280                        result = new MessagingException(
1281                                String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1282                    }
1283                    // discard progress reports that look like sentinels
1284                    if (progress < 0 || progress >= 100) {
1285                        return;
1286                    }
1287                    break;
1288            }
1289            synchronized (mListeners) {
1290                for (Result listener : mListeners) {
1291                    listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
1292                }
1293            }
1294        }
1295
1296        /**
1297         * Note, this is an incomplete implementation of this callback, because we are
1298         * not getting things back from Service in quite the same way as from MessagingController.
1299         * However, this is sufficient for basic "progress=100" notification that message send
1300         * has just completed.
1301         */
1302        public void sendMessageStatus(long messageId, String subject, int statusCode,
1303                int progress) {
1304//            Log.d(Email.LOG_TAG, "sendMessageStatus: messageId=" + messageId
1305//                    + " statusCode=" + statusCode + " progress=" + progress);
1306//            Log.d(Email.LOG_TAG, "sendMessageStatus: subject=" + subject);
1307            long accountId = -1;        // This should be in the callback
1308            MessagingException result = mapStatusToException(statusCode);
1309            switch (statusCode) {
1310                case EmailServiceStatus.SUCCESS:
1311                    progress = 100;
1312                    break;
1313                case EmailServiceStatus.IN_PROGRESS:
1314                    // discard progress reports that look like sentinels
1315                    if (progress < 0 || progress >= 100) {
1316                        return;
1317                    }
1318                    break;
1319            }
1320//            Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId
1321//                    + " progress=" + progress);
1322            synchronized(mListeners) {
1323                for (Result listener : mListeners) {
1324                    listener.sendMailCallback(result, accountId, messageId, progress);
1325                }
1326            }
1327        }
1328
1329        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1330            MessagingException result = mapStatusToException(statusCode);
1331            switch (statusCode) {
1332                case EmailServiceStatus.SUCCESS:
1333                    progress = 100;
1334                    break;
1335                case EmailServiceStatus.IN_PROGRESS:
1336                    // discard progress reports that look like sentinels
1337                    if (progress < 0 || progress >= 100) {
1338                        return;
1339                    }
1340                    break;
1341            }
1342            synchronized(mListeners) {
1343                for (Result listener : mListeners) {
1344                    listener.updateMailboxListCallback(result, accountId, progress);
1345                }
1346            }
1347        }
1348
1349        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1350            MessagingException result = mapStatusToException(statusCode);
1351            switch (statusCode) {
1352                case EmailServiceStatus.SUCCESS:
1353                    progress = 100;
1354                    break;
1355                case EmailServiceStatus.IN_PROGRESS:
1356                    // discard progress reports that look like sentinels
1357                    if (progress < 0 || progress >= 100) {
1358                        return;
1359                    }
1360                    break;
1361            }
1362            // TODO where do we get "number of new messages" as well?
1363            // TODO should pass this back instead of looking it up here
1364            // TODO smaller projection
1365            Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1366            // The mailbox could have disappeared if the server commanded it
1367            if (mbx == null) return;
1368            long accountId = mbx.mAccountKey;
1369            synchronized(mListeners) {
1370                for (Result listener : mListeners) {
1371                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
1372                }
1373            }
1374        }
1375
1376        private MessagingException mapStatusToException(int statusCode) {
1377            switch (statusCode) {
1378                case EmailServiceStatus.SUCCESS:
1379                case EmailServiceStatus.IN_PROGRESS:
1380                    return null;
1381
1382                case EmailServiceStatus.LOGIN_FAILED:
1383                    return new AuthenticationFailedException("");
1384
1385                case EmailServiceStatus.CONNECTION_ERROR:
1386                    return new MessagingException(MessagingException.IOERROR);
1387
1388                case EmailServiceStatus.SECURITY_FAILURE:
1389                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1390
1391                case EmailServiceStatus.MESSAGE_NOT_FOUND:
1392                case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1393                case EmailServiceStatus.FOLDER_NOT_DELETED:
1394                case EmailServiceStatus.FOLDER_NOT_RENAMED:
1395                case EmailServiceStatus.FOLDER_NOT_CREATED:
1396                case EmailServiceStatus.REMOTE_EXCEPTION:
1397                    // TODO: define exception code(s) & UI string(s) for server-side errors
1398                default:
1399                    return new MessagingException(String.valueOf(statusCode));
1400            }
1401        }
1402    }
1403}
1404