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