Controller.java revision e1a6088ee4e3f0e4344dd9bc38029b6d01431eab
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        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) return;
319        Utility.runAsync(new Runnable() {
320            @Override
321            public void run() {
322                final IEmailService service = getServiceForAccount(accountId);
323                if (service != null) {
324                    // Service implementation
325                    try {
326                        service.updateFolderList(accountId);
327                    } catch (RemoteException e) {
328                        // TODO Change exception handling to be consistent with however this method
329                        // is implemented for other protocols
330                        Log.d("updateMailboxList", "RemoteException" + e);
331                    }
332                } else {
333                    throw new IllegalStateException("No service for updateMailboxList?");
334                }
335            }
336        });
337    }
338
339    /**
340     * Request a remote update of a mailbox.
341     *
342     * The contract here should be to try and update the headers ASAP, in order to populate
343     * a simple message list.  We should also at this point queue up a background task of
344     * downloading some/all of the messages in this mailbox, but that should be interruptable.
345     */
346    public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) {
347
348        IEmailService service = getServiceForAccount(accountId);
349        if (service != null) {
350           try {
351                service.startSync(mailboxId, userRequest);
352            } catch (RemoteException e) {
353                // TODO Change exception handling to be consistent with however this method
354                // is implemented for other protocols
355                Log.d("updateMailbox", "RemoteException" + e);
356            }
357         } else {
358             throw new IllegalStateException("No service for loadMessageForView?");
359         }
360    }
361
362    /**
363     * Request that any final work necessary be done, to load a message.
364     *
365     * Note, this assumes that the caller has already checked message.mFlagLoaded and that
366     * additional work is needed.  There is no optimization here for a message which is already
367     * loaded.
368     *
369     * @param messageId the message to load
370     * @param callback the Controller callback by which results will be reported
371     */
372    public void loadMessageForView(final long messageId) {
373
374        // Split here for target type (Service or MessagingController)
375        EmailServiceProxy service = getServiceForMessage(messageId);
376        if (service.isRemote()) {
377            // There is no service implementation, so we'll just jam the value, log the error,
378            // and get out of here.
379            Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
380            ContentValues cv = new ContentValues();
381            cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
382            mProviderContext.getContentResolver().update(uri, cv, null, null);
383            Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for remote service message.");
384            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
385            synchronized (mListeners) {
386                for (Result listener : mListeners) {
387                    listener.loadMessageForViewCallback(null, accountId, messageId, 100);
388                }
389            }
390        } else {
391            try {
392                service.loadMore(messageId);
393            } catch (RemoteException e) {
394            }
395        }
396    }
397
398
399    /**
400     * Saves the message to a mailbox of given type.
401     * This is a synchronous operation taking place in the same thread as the caller.
402     * Upon return the message.mId is set.
403     * @param message the message (must have the mAccountId set).
404     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
405     */
406    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
407        long accountId = message.mAccountKey;
408        long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
409        message.mMailboxKey = mailboxId;
410        message.save(mProviderContext);
411    }
412
413    /**
414     * Look for a specific system mailbox, creating it if necessary, and return the mailbox id.
415     * This is a blocking operation and should not be called from the UI thread.
416     *
417     * Synchronized so multiple threads can call it (and not risk creating duplicate boxes).
418     *
419     * @param accountId the account id
420     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
421     * @return the id of the mailbox. The mailbox is created if not existing.
422     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
423     * Does not validate the input in other ways (e.g. does not verify the existence of account).
424     */
425    public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) {
426        if (accountId < 0 || mailboxType < 0) {
427            return Mailbox.NO_MAILBOX;
428        }
429        long mailboxId =
430            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
431        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
432    }
433
434    /**
435     * Returns the server-side name for a specific mailbox.
436     *
437     * @return the resource string corresponding to the mailbox type, empty if not found.
438     */
439    public static String getMailboxServerName(Context context, int mailboxType) {
440        int resId = -1;
441        switch (mailboxType) {
442            case Mailbox.TYPE_INBOX:
443                resId = R.string.mailbox_name_server_inbox;
444                break;
445            case Mailbox.TYPE_OUTBOX:
446                resId = R.string.mailbox_name_server_outbox;
447                break;
448            case Mailbox.TYPE_DRAFTS:
449                resId = R.string.mailbox_name_server_drafts;
450                break;
451            case Mailbox.TYPE_TRASH:
452                resId = R.string.mailbox_name_server_trash;
453                break;
454            case Mailbox.TYPE_SENT:
455                resId = R.string.mailbox_name_server_sent;
456                break;
457            case Mailbox.TYPE_JUNK:
458                resId = R.string.mailbox_name_server_junk;
459                break;
460        }
461        return resId != -1 ? context.getString(resId) : "";
462    }
463
464    /**
465     * Create a mailbox given the account and mailboxType.
466     * TODO: Does this need to be signaled explicitly to the sync engines?
467     */
468    @VisibleForTesting
469    long createMailbox(long accountId, int mailboxType) {
470        if (accountId < 0 || mailboxType < 0) {
471            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
472            Log.e(Logging.LOG_TAG, mes);
473            throw new RuntimeException(mes);
474        }
475        Mailbox box = Mailbox.newSystemMailbox(
476                accountId, mailboxType, getMailboxServerName(mContext, mailboxType));
477        box.save(mProviderContext);
478        return box.mId;
479    }
480
481    /**
482     * Send a message:
483     * - move the message to Outbox (the message is assumed to be in Drafts).
484     * - EAS service will take it from there
485     * - mark reply/forward state in source message (if any)
486     * - trigger send for POP/IMAP
487     * @param message the fully populated Message (usually retrieved from the Draft box). Note that
488     *     all transient fields (e.g. Body related fields) are also expected to be fully loaded
489     */
490    public void sendMessage(Message message) {
491        ContentResolver resolver = mProviderContext.getContentResolver();
492        long accountId = message.mAccountKey;
493        long messageId = message.mId;
494        if (accountId == Account.NO_ACCOUNT) {
495            accountId = lookupAccountForMessage(messageId);
496        }
497        if (accountId == Account.NO_ACCOUNT) {
498            // probably the message was not found
499            if (Logging.LOGD) {
500                Email.log("no account found for message " + messageId);
501            }
502            return;
503        }
504
505        // Move to Outbox
506        long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
507        ContentValues cv = new ContentValues();
508        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
509
510        // does this need to be SYNCED_CONTENT_URI instead?
511        Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
512        resolver.update(uri, cv, null, null);
513
514        // If this is a reply/forward, indicate it as such on the source.
515        long sourceKey = message.mSourceKey;
516        if (sourceKey != Message.NO_MESSAGE) {
517            boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0;
518            int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED;
519            setMessageAnsweredOrForwarded(sourceKey, flagUpdate);
520        }
521
522        sendPendingMessages(accountId);
523    }
524
525    public void sendPendingMessages(long accountId) {
526        EmailServiceProxy service =
527            EmailServiceUtils.getServiceForAccount(mContext, null, accountId);
528        try {
529            service.sendMail(accountId);
530        } catch (RemoteException e) {
531        }
532    }
533
534    /**
535     * Reset visible limits for all accounts.
536     * For each account:
537     *   look up limit
538     *   write limit into all mailboxes for that account
539     */
540    @SuppressWarnings("deprecation")
541    public void resetVisibleLimits() {
542        Utility.runAsync(new Runnable() {
543            @Override
544            public void run() {
545                ContentResolver resolver = mProviderContext.getContentResolver();
546                Cursor c = null;
547                try {
548                    c = resolver.query(
549                            Account.CONTENT_URI,
550                            Account.ID_PROJECTION,
551                            null, null, null);
552                    while (c.moveToNext()) {
553                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
554                        String protocol = Account.getProtocol(mProviderContext, accountId);
555                        if (!HostAuth.SCHEME_EAS.equals(protocol)) {
556                            ContentValues cv = new ContentValues();
557                            cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT);
558                            resolver.update(Mailbox.CONTENT_URI, cv,
559                                    MailboxColumns.ACCOUNT_KEY + "=?",
560                                    new String[] { Long.toString(accountId) });
561                        }
562                    }
563                } finally {
564                    if (c != null) {
565                        c.close();
566                    }
567                }
568            }
569        });
570    }
571
572    /**
573     * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
574     * IMAP and POP mailboxes, with the exception of the EAS search mailbox.
575     *
576     * @param mailboxId the mailbox
577     */
578    public void loadMoreMessages(final long mailboxId) {
579        EmailAsyncTask.runAsyncParallel(new Runnable() {
580            @Override
581            public void run() {
582                Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
583                if (mailbox == null) {
584                    return;
585                }
586                if (mailbox.mType == Mailbox.TYPE_SEARCH) {
587                    try {
588                        searchMore(mailbox.mAccountKey);
589                    } catch (MessagingException e) {
590                        // Nothing to be done
591                    }
592                    return;
593                }
594                Account account = Account.restoreAccountWithId(mProviderContext,
595                        mailbox.mAccountKey);
596                if (account == null) {
597                    return;
598                }
599                // Use provider math to increment the field
600                ContentValues cv = new ContentValues();;
601                cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
602                cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT);
603                Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
604                mProviderContext.getContentResolver().update(uri, cv, null, null);
605                // Trigger a refresh using the new, longer limit
606                mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT;
607                updateMailbox(account.mId, mailboxId, true);
608            }
609        });
610    }
611
612    /**
613     * @param messageId the id of message
614     * @return the accountId corresponding to the given messageId, or -1 if not found.
615     */
616    private long lookupAccountForMessage(long messageId) {
617        ContentResolver resolver = mProviderContext.getContentResolver();
618        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
619                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
620                                  new String[] { Long.toString(messageId) }, null);
621        try {
622            return c.moveToFirst()
623                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
624                : -1;
625        } finally {
626            c.close();
627        }
628    }
629
630    /**
631     * Delete a single attachment entry from the DB given its id.
632     * Does not delete any eventual associated files.
633     */
634    public void deleteAttachment(long attachmentId) {
635        ContentResolver resolver = mProviderContext.getContentResolver();
636        Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
637        resolver.delete(uri, null, null);
638    }
639
640    /**
641     * Async version of {@link #deleteMessageSync}.
642     */
643    public void deleteMessage(final long messageId) {
644        EmailAsyncTask.runAsyncParallel(new Runnable() {
645            @Override
646            public void run() {
647                deleteMessageSync(messageId);
648            }
649        });
650    }
651
652    /**
653     * Batch & async version of {@link #deleteMessageSync}.
654     */
655    public void deleteMessages(final long[] messageIds) {
656        if (messageIds == null || messageIds.length == 0) {
657            throw new IllegalArgumentException();
658        }
659        EmailAsyncTask.runAsyncParallel(new Runnable() {
660            @Override
661            public void run() {
662                for (long messageId: messageIds) {
663                    deleteMessageSync(messageId);
664                }
665            }
666        });
667    }
668
669    /**
670     * Delete a single message by moving it to the trash, or really delete it if it's already in
671     * trash or a draft message.
672     *
673     * This function has no callback, no result reporting, because the desired outcome
674     * is reflected entirely by changes to one or more cursors.
675     *
676     * @param messageId The id of the message to "delete".
677     */
678    /* package */ void deleteMessageSync(long messageId) {
679        // 1. Get the message's account
680        Account account = Account.getAccountForMessageId(mProviderContext, messageId);
681
682        if (account == null) return;
683
684        // 2. Confirm that there is a trash mailbox available.  If not, create one
685        long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH);
686
687        // 3. Get the message's original mailbox
688        Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId);
689
690        if (mailbox == null) return;
691
692        // 4.  Drop non-essential data for the message (e.g. attachment files)
693        AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId,
694                messageId);
695
696        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI,
697                messageId);
698        ContentResolver resolver = mProviderContext.getContentResolver();
699
700        // 5. Perform "delete" as appropriate
701        if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) {
702            // 5a. Really delete it
703            resolver.delete(uri, null, null);
704        } else {
705            // 5b. Move to trash
706            ContentValues cv = new ContentValues();
707            cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
708            resolver.update(uri, cv, null, null);
709        }
710    }
711
712    /**
713     * Moves messages to a new mailbox.
714     *
715     * This function has no callback, no result reporting, because the desired outcome
716     * is reflected entirely by changes to one or more cursors.
717     *
718     * Note this method assumes all of the given message and mailbox IDs belong to the same
719     * account.
720     *
721     * @param messageIds IDs of the messages that are to be moved
722     * @param newMailboxId ID of the new mailbox that the messages will be moved to
723     * @return an asynchronous task that executes the move (for testing only)
724     */
725    public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds,
726            final long newMailboxId) {
727        if (messageIds == null || messageIds.length == 0) {
728            throw new IllegalArgumentException();
729        }
730        return EmailAsyncTask.runAsyncParallel(new Runnable() {
731            @Override
732            public void run() {
733                Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]);
734                if (account != null) {
735                    ContentValues cv = new ContentValues();
736                    cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId);
737                    ContentResolver resolver = mProviderContext.getContentResolver();
738                    for (long messageId : messageIds) {
739                        Uri uri = ContentUris.withAppendedId(
740                                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
741                        resolver.update(uri, cv, null, null);
742                    }
743                }
744            }
745        });
746    }
747
748    /**
749     * Set/clear the unread status of a message
750     *
751     * @param messageId the message to update
752     * @param isRead the new value for the isRead flag
753     */
754    public void setMessageReadSync(long messageId, boolean isRead) {
755        setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
756    }
757
758    /**
759     * Set/clear the unread status of a message from UI thread
760     *
761     * @param messageId the message to update
762     * @param isRead the new value for the isRead flag
763     * @return the EmailAsyncTask created
764     */
765    public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId,
766            final boolean isRead) {
767        return EmailAsyncTask.runAsyncParallel(new Runnable() {
768            @Override
769            public void run() {
770                setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
771            }});
772    }
773
774    /**
775     * Update a message record and ping MessagingController, if necessary
776     *
777     * @param messageId the message to update
778     * @param cv the ContentValues used in the update
779     */
780    private void updateMessageSync(long messageId, ContentValues cv) {
781        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
782        mProviderContext.getContentResolver().update(uri, cv, null, null);
783    }
784
785    /**
786     * Set the answered status of a message
787     *
788     * @param messageId the message to update
789     * @return the AsyncTask that will execute the changes (for testing only)
790     */
791    public void setMessageAnsweredOrForwarded(final long messageId,
792            final int flag) {
793        EmailAsyncTask.runAsyncParallel(new Runnable() {
794            @Override
795            public void run() {
796                Message msg = Message.restoreMessageWithId(mProviderContext, messageId);
797                if (msg == null) {
798                    Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward");
799                    return;
800                }
801                ContentValues cv = new ContentValues();
802                cv.put(MessageColumns.FLAGS, msg.mFlags | flag);
803                updateMessageSync(messageId, cv);
804            }
805        });
806    }
807
808    /**
809     * Set/clear the favorite status of a message from UI thread
810     *
811     * @param messageId the message to update
812     * @param isFavorite the new value for the isFavorite flag
813     * @return the EmailAsyncTask created
814     */
815    public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId,
816            final boolean isFavorite) {
817        return EmailAsyncTask.runAsyncParallel(new Runnable() {
818            @Override
819            public void run() {
820                setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE,
821                        isFavorite);
822            }});
823    }
824    /**
825     * Set/clear the favorite status of a message
826     *
827     * @param messageId the message to update
828     * @param isFavorite the new value for the isFavorite flag
829     */
830    public void setMessageFavoriteSync(long messageId, boolean isFavorite) {
831        setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
832    }
833
834    /**
835     * Set/clear boolean columns of a message
836     *
837     * @param messageId the message to update
838     * @param columnName the column to update
839     * @param columnValue the new value for the column
840     */
841    private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) {
842        ContentValues cv = new ContentValues();
843        cv.put(columnName, columnValue);
844        updateMessageSync(messageId, cv);
845    }
846
847
848    private static final HashMap<Long, SearchParams> sSearchParamsMap =
849        new HashMap<Long, SearchParams>();
850
851    public void searchMore(long accountId) throws MessagingException {
852        SearchParams params = sSearchParamsMap.get(accountId);
853        if (params == null) return;
854        params.mOffset += params.mLimit;
855        searchMessages(accountId, params);
856    }
857
858    /**
859     * Search for messages on the (IMAP) server; do not call this on the UI thread!
860     * @param accountId the id of the account to be searched
861     * @param searchParams the parameters for this search
862     * @throws MessagingException
863     */
864    public int searchMessages(final long accountId, final SearchParams searchParams)
865            throws MessagingException {
866        // Find/create our search mailbox
867        Mailbox searchMailbox = getSearchMailbox(accountId);
868        if (searchMailbox == null) return 0;
869        final long searchMailboxId = searchMailbox.mId;
870        // Save this away (per account)
871        sSearchParamsMap.put(accountId, searchParams);
872
873        if (searchParams.mOffset == 0) {
874            // Delete existing contents of search mailbox
875            ContentResolver resolver = mContext.getContentResolver();
876            resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
877                    null);
878            ContentValues cv = new ContentValues();
879            // For now, use the actual query as the name of the mailbox
880            cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter);
881            resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
882                    cv, null, null);
883        }
884
885        IEmailService service = getServiceForAccount(accountId);
886        if (service != null) {
887            // Service implementation
888            try {
889                return service.searchMessages(accountId, searchParams, searchMailboxId);
890            } catch (RemoteException e) {
891                // TODO Change exception handling to be consistent with however this method
892                // is implemented for other protocols
893                Log.e("searchMessages", "RemoteException", e);
894            }
895        }
896        return 0;
897    }
898
899    private EmailServiceProxy getServiceForAccount(long accountId) {
900        return EmailServiceUtils.getServiceForAccount(mContext, mServiceCallback, accountId);
901    }
902
903    /**
904     * Respond to a meeting invitation.
905     *
906     * @param messageId the id of the invitation being responded to
907     * @param response the code representing the response to the invitation
908     */
909    public void sendMeetingResponse(final long messageId, final int response) {
910         // Split here for target type (Service or MessagingController)
911        IEmailService service = getServiceForMessage(messageId);
912        if (service != null) {
913            // Service implementation
914            try {
915                service.sendMeetingResponse(messageId, response);
916            } catch (RemoteException e) {
917                // TODO Change exception handling to be consistent with however this method
918                // is implemented for other protocols
919                Log.e("onDownloadAttachment", "RemoteException", e);
920            }
921        }
922    }
923
924    /**
925     * Request that an attachment be loaded.  It will be stored at a location controlled
926     * by the AttachmentProvider.
927     *
928     * @param attachmentId the attachment to load
929     * @param messageId the owner message
930     * @param accountId the owner account
931     */
932    public void loadAttachment(final long attachmentId, final long messageId,
933            final long accountId) {
934        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
935        if (attachInfo == null) {
936            return;
937        }
938
939        if (Utility.attachmentExists(mProviderContext, attachInfo)) {
940            // The attachment has already been downloaded, so we will just "pretend" to download it
941            // This presumably is for POP3 messages
942            synchronized (mListeners) {
943                for (Result listener : mListeners) {
944                    listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
945                }
946                for (Result listener : mListeners) {
947                    listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
948                }
949            }
950            return;
951        }
952
953        // Flag the attachment as needing download at the user's request
954        ContentValues cv = new ContentValues();
955        cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
956        attachInfo.update(mProviderContext, cv);
957    }
958
959    /**
960     * For a given message id, return a service proxy if applicable, or null.
961     *
962     * @param messageId the message of interest
963     * @result service proxy, or null if n/a
964     */
965    private EmailServiceProxy getServiceForMessage(long messageId) {
966        // TODO make this more efficient, caching the account, smaller lookup here, etc.
967        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
968        if (message == null) {
969            return null;
970        }
971        return getServiceForAccount(message.mAccountKey);
972    }
973
974    /**
975     * Delete an account.
976     */
977    public void deleteAccount(final long accountId) {
978        EmailAsyncTask.runAsyncParallel(new Runnable() {
979            @Override
980            public void run() {
981                deleteAccountSync(accountId, mProviderContext);
982            }
983        });
984    }
985
986    /**
987     * Delete an account synchronously.
988     */
989    public void deleteAccountSync(long accountId, Context context) {
990        try {
991            mLegacyControllerMap.remove(accountId);
992            // Get the account URI.
993            final Account account = Account.restoreAccountWithId(context, accountId);
994            if (account == null) {
995                return; // Already deleted?
996            }
997
998            // Delete account data, attachments, PIM data, etc.
999            deleteSyncedDataSync(accountId);
1000
1001            // Now delete the account itself
1002            Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
1003            context.getContentResolver().delete(uri, null, null);
1004
1005            // For unit tests, don't run backup, security, and ui pieces.
1006            if (mInUnitTests) {
1007                return;
1008            }
1009
1010            // Clean up
1011            AccountBackupRestore.backup(context);
1012            SecurityPolicy.getInstance(context).reducePolicies();
1013            Email.setServicesEnabledSync(context);
1014            Email.setNotifyUiAccountsChanged(true);
1015        } catch (Exception e) {
1016            Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
1017        }
1018    }
1019
1020    /**
1021     * Delete all synced data, but don't delete the actual account.  This is used when security
1022     * policy requirements are not met, and we don't want to reveal any synced data, but we do
1023     * wish to keep the account configured (e.g. to accept remote wipe commands).
1024     *
1025     * The only mailbox not deleted is the account mailbox (if any)
1026     * Also, clear the sync keys on the remaining account, since the data is gone.
1027     *
1028     * SYNCHRONOUS - do not call from UI thread.
1029     *
1030     * @param accountId The account to wipe.
1031     */
1032    public void deleteSyncedDataSync(long accountId) {
1033        try {
1034            // Delete synced attachments
1035            AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext,
1036                    accountId);
1037
1038            // Delete synced email, leaving only an empty inbox.  We do this in two phases:
1039            // 1. Delete all non-inbox mailboxes (which will delete all of their messages)
1040            // 2. Delete all remaining messages (which will be the inbox messages)
1041            ContentResolver resolver = mProviderContext.getContentResolver();
1042            String[] accountIdArgs = new String[] { Long.toString(accountId) };
1043            resolver.delete(Mailbox.CONTENT_URI,
1044                    MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION,
1045                    accountIdArgs);
1046            resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs);
1047
1048            // Delete sync keys on remaining items
1049            ContentValues cv = new ContentValues();
1050            cv.putNull(Account.SYNC_KEY);
1051            resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
1052            cv.clear();
1053            cv.putNull(Mailbox.SYNC_KEY);
1054            resolver.update(Mailbox.CONTENT_URI, cv,
1055                    MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
1056
1057            // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
1058            IEmailService service = getServiceForAccount(accountId);
1059            if (service != null) {
1060                service.deleteAccountPIMData(accountId);
1061            }
1062        } catch (Exception e) {
1063            Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e);
1064        }
1065    }
1066
1067    /**
1068     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
1069     * and the result is observed via provider cursors.  The callback will *not* necessarily be
1070     * made from the UI thread, so you may need further handlers to safely make UI updates.
1071     */
1072    public static abstract class Result {
1073        private volatile boolean mRegistered;
1074
1075        protected void setRegistered(boolean registered) {
1076            mRegistered = registered;
1077        }
1078
1079        protected final boolean isRegistered() {
1080            return mRegistered;
1081        }
1082
1083        /**
1084         * Callback for updateMailboxList
1085         *
1086         * @param result If null, the operation completed without error
1087         * @param accountId The account being operated on
1088         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1089         */
1090        public void updateMailboxListCallback(MessagingException result, long accountId,
1091                int progress) {
1092        }
1093
1094        /**
1095         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
1096         * it's a separate call used only by UI's, so we can keep things separate.
1097         *
1098         * @param result If null, the operation completed without error
1099         * @param accountId The account being operated on
1100         * @param mailboxId The mailbox being operated on
1101         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1102         * @param numNewMessages the number of new messages delivered
1103         */
1104        public void updateMailboxCallback(MessagingException result, long accountId,
1105                long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) {
1106        }
1107
1108        /**
1109         * Callback for loadMessageForView
1110         *
1111         * @param result if null, the attachment completed - if non-null, terminating with failure
1112         * @param messageId the message which contains the attachment
1113         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1114         */
1115        public void loadMessageForViewCallback(MessagingException result, long accountId,
1116                long messageId, int progress) {
1117        }
1118
1119        /**
1120         * Callback for loadAttachment
1121         *
1122         * @param result if null, the attachment completed - if non-null, terminating with failure
1123         * @param messageId the message which contains the attachment
1124         * @param attachmentId the attachment being loaded
1125         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1126         */
1127        public void loadAttachmentCallback(MessagingException result, long accountId,
1128                long messageId, long attachmentId, int progress) {
1129        }
1130
1131        /**
1132         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
1133         * it's a separate call used only by the automatic checker service, so we can keep
1134         * things separate.
1135         *
1136         * @param result If null, the operation completed without error
1137         * @param accountId The account being operated on
1138         * @param mailboxId The mailbox being operated on (may be unknown at start)
1139         * @param progress 0 for "starting", no updates, 100 for complete
1140         * @param tag the same tag that was passed to serviceCheckMail()
1141         */
1142        public void serviceCheckMailCallback(MessagingException result, long accountId,
1143                long mailboxId, int progress, long tag) {
1144        }
1145    }
1146
1147    /**
1148     * Service callback for service operations
1149     */
1150    private class ServiceCallback extends IEmailServiceCallback.Stub {
1151
1152        @Override
1153        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1154                int progress) {
1155            MessagingException result = mapStatusToException(statusCode);
1156            switch (statusCode) {
1157                case EmailServiceStatus.SUCCESS:
1158                    progress = 100;
1159                    break;
1160                case EmailServiceStatus.IN_PROGRESS:
1161                    // discard progress reports that look like sentinels
1162                    if (progress < 0 || progress >= 100) {
1163                        return;
1164                    }
1165                    break;
1166            }
1167            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1168            synchronized (mListeners) {
1169                for (Result listener : mListeners) {
1170                    listener.loadAttachmentCallback(result, accountId, messageId, attachmentId,
1171                            progress);
1172                }
1173            }
1174        }
1175
1176        /**
1177         * Unused
1178         */
1179        @Override
1180        public void sendMessageStatus(long messageId, String subject, int statusCode,
1181                int progress) {
1182        }
1183
1184        /**
1185         * Note, this is an incomplete implementation of this callback, because we are
1186         * not getting things back from Service in quite the same way as from MessagingController.
1187         * However, this is sufficient for basic "progress=100" notification that message send
1188         * has just completed.
1189         */
1190        @Override
1191        public void loadMessageStatus(long messageId, int statusCode, int progress) {
1192            long accountId = -1;        // This should be in the callback
1193            MessagingException result = mapStatusToException(statusCode);
1194            switch (statusCode) {
1195                case EmailServiceStatus.SUCCESS:
1196                    progress = 100;
1197                    break;
1198                case EmailServiceStatus.IN_PROGRESS:
1199                    // discard progress reports that look like sentinels
1200                    if (progress < 0 || progress >= 100) {
1201                        return;
1202                    }
1203                    break;
1204            }
1205            synchronized(mListeners) {
1206                for (Result listener : mListeners) {
1207                    listener.loadMessageForViewCallback(result, accountId, messageId, progress);
1208                }
1209            }
1210        }
1211
1212        @Override
1213        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1214            MessagingException result = mapStatusToException(statusCode);
1215            switch (statusCode) {
1216                case EmailServiceStatus.SUCCESS:
1217                    progress = 100;
1218                    break;
1219                case EmailServiceStatus.IN_PROGRESS:
1220                    // discard progress reports that look like sentinels
1221                    if (progress < 0 || progress >= 100) {
1222                        return;
1223                    }
1224                    break;
1225            }
1226            synchronized(mListeners) {
1227                for (Result listener : mListeners) {
1228                    listener.updateMailboxListCallback(result, accountId, progress);
1229                }
1230            }
1231        }
1232
1233        @Override
1234        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1235            MessagingException result = mapStatusToException(statusCode);
1236            switch (statusCode) {
1237                case EmailServiceStatus.SUCCESS:
1238                    progress = 100;
1239                    break;
1240                case EmailServiceStatus.IN_PROGRESS:
1241                    // discard progress reports that look like sentinels
1242                    if (progress < 0 || progress >= 100) {
1243                        return;
1244                    }
1245                    break;
1246            }
1247            // TODO should pass this back instead of looking it up here
1248            Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1249            // The mailbox could have disappeared if the server commanded it
1250            if (mbx == null) return;
1251            long accountId = mbx.mAccountKey;
1252            synchronized(mListeners) {
1253                for (Result listener : mListeners) {
1254                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null);
1255                }
1256            }
1257        }
1258
1259        private MessagingException mapStatusToException(int statusCode) {
1260            switch (statusCode) {
1261                case EmailServiceStatus.SUCCESS:
1262                case EmailServiceStatus.IN_PROGRESS:
1263                // Don't generate error if the account is uninitialized
1264                case EmailServiceStatus.ACCOUNT_UNINITIALIZED:
1265                    return null;
1266
1267                case EmailServiceStatus.LOGIN_FAILED:
1268                    return new AuthenticationFailedException("");
1269
1270                case EmailServiceStatus.CONNECTION_ERROR:
1271                    return new MessagingException(MessagingException.IOERROR);
1272
1273                case EmailServiceStatus.SECURITY_FAILURE:
1274                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1275
1276                case EmailServiceStatus.ACCESS_DENIED:
1277                    return new MessagingException(MessagingException.ACCESS_DENIED);
1278
1279                case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1280                    return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND);
1281
1282                case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR:
1283                    return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR);
1284
1285                case EmailServiceStatus.MESSAGE_NOT_FOUND:
1286                case EmailServiceStatus.FOLDER_NOT_DELETED:
1287                case EmailServiceStatus.FOLDER_NOT_RENAMED:
1288                case EmailServiceStatus.FOLDER_NOT_CREATED:
1289                case EmailServiceStatus.REMOTE_EXCEPTION:
1290                    // TODO: define exception code(s) & UI string(s) for server-side errors
1291                default:
1292                    return new MessagingException(String.valueOf(statusCode));
1293            }
1294        }
1295    }
1296
1297    private interface ServiceCallbackWrapper {
1298        public void call(IEmailServiceCallback cb) throws RemoteException;
1299    }
1300
1301    /**
1302     * Proxy that can be used to broadcast service callbacks; we currently use this only for
1303     * loadAttachment callbacks
1304     */
1305    private final IEmailServiceCallback.Stub mCallbackProxy = new IEmailServiceCallback.Stub() {
1306
1307        /**
1308         * Broadcast a callback to the everyone that's registered
1309         *
1310         * @param wrapper the ServiceCallbackWrapper used in the broadcast
1311         */
1312        private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
1313            if (sCallbackList != null) {
1314                // Call everyone on our callback list
1315                // Exceptions can be safely ignored
1316                int count = sCallbackList.beginBroadcast();
1317                for (int i = 0; i < count; i++) {
1318                    try {
1319                        wrapper.call(sCallbackList.getBroadcastItem(i));
1320                    } catch (RemoteException e) {
1321                    }
1322                }
1323                sCallbackList.finishBroadcast();
1324            }
1325        }
1326
1327        @Override
1328        public void loadAttachmentStatus(final long messageId, final long attachmentId,
1329                final int status, final int progress) {
1330            broadcastCallback(new ServiceCallbackWrapper() {
1331                @Override
1332                public void call(IEmailServiceCallback cb) throws RemoteException {
1333                    cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
1334                }
1335            });
1336        }
1337
1338        @Override
1339        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
1340                throws RemoteException {
1341        }
1342
1343        @Override
1344        public void syncMailboxStatus(final long mailboxId, final int statusCode,
1345                final int progress) throws RemoteException {
1346        }
1347
1348        @Override
1349        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
1350                throws RemoteException {
1351        }
1352
1353        @Override
1354        public void loadMessageStatus(long messageId, int statusCode, int progress)
1355                throws RemoteException {
1356        }
1357    };
1358}
1359