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