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