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