Controller.java revision c4cdb11d24c19428dd39f986b00c1a29e75e1505
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email;
18
19import com.android.email.mail.Store;
20import com.android.email.mail.store.Pop3Store.Pop3Message;
21import com.android.emailcommon.Api;
22import com.android.emailcommon.Logging;
23import com.android.emailcommon.mail.AuthenticationFailedException;
24import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
25import com.android.emailcommon.mail.MessagingException;
26import com.android.emailcommon.provider.EmailContent;
27import com.android.emailcommon.provider.EmailContent.Account;
28import com.android.emailcommon.provider.EmailContent.Attachment;
29import com.android.emailcommon.provider.EmailContent.Body;
30import com.android.emailcommon.provider.EmailContent.Mailbox;
31import com.android.emailcommon.provider.EmailContent.MailboxColumns;
32import com.android.emailcommon.provider.EmailContent.Message;
33import com.android.emailcommon.provider.EmailContent.MessageColumns;
34import com.android.emailcommon.service.EmailServiceStatus;
35import com.android.emailcommon.service.IEmailService;
36import com.android.emailcommon.service.IEmailServiceCallback;
37import com.android.emailcommon.utility.AttachmentUtilities;
38import com.android.emailcommon.utility.EmailAsyncTask;
39import com.android.emailcommon.utility.Utility;
40import com.google.common.annotations.VisibleForTesting;
41
42import android.app.Service;
43import android.content.ContentResolver;
44import android.content.ContentUris;
45import android.content.ContentValues;
46import android.content.Context;
47import android.content.Intent;
48import android.database.Cursor;
49import android.net.Uri;
50import android.os.AsyncTask;
51import android.os.Bundle;
52import android.os.IBinder;
53import android.os.RemoteCallbackList;
54import android.os.RemoteException;
55import android.util.Log;
56
57import java.io.FileNotFoundException;
58import java.io.IOException;
59import java.io.InputStream;
60import java.security.InvalidParameterException;
61import java.util.ArrayList;
62import java.util.Collection;
63import java.util.HashSet;
64import java.util.concurrent.ConcurrentHashMap;
65
66/**
67 * New central controller/dispatcher for Email activities that may require remote operations.
68 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
69 * based code.  We implement Service to allow loadAttachment calls to be sent in a consistent manner
70 * to IMAP, POP3, and EAS by AttachmentDownloadService
71 */
72public class Controller {
73    private static final String TAG = "Controller";
74    private static Controller sInstance;
75    private final Context mContext;
76    private Context mProviderContext;
77    private final MessagingController mLegacyController;
78    private final LegacyListener mLegacyListener = new LegacyListener();
79    private final ServiceCallback mServiceCallback = new ServiceCallback();
80    private final HashSet<Result> mListeners = new HashSet<Result>();
81    /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap =
82        new ConcurrentHashMap<Long, Boolean>();
83
84    // Note that 0 is a syntactically valid account key; however there can never be an account
85    // with id = 0, so attempts to restore the account will return null.  Null values are
86    // handled properly within the code, so this won't cause any issues.
87    private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0;
88    /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__";
89    /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__";
90    /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
91    private static final String WHERE_TYPE_ATTACHMENT =
92        MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT;
93    private static final String WHERE_TYPE_SEARCH =
94        MailboxColumns.TYPE + "=" + Mailbox.TYPE_SEARCH;
95    private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?";
96
97    private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
98        EmailContent.RECORD_ID,
99        EmailContent.MessageColumns.ACCOUNT_KEY
100    };
101    private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
102
103    private static final String[] BODY_SOURCE_KEY_PROJECTION =
104        new String[] {Body.SOURCE_MESSAGE_KEY};
105    private static final int BODY_SOURCE_KEY_COLUMN = 0;
106    private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
107
108    private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
109    private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION =
110        MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" +
111        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
112    private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?";
113
114    // Service callbacks as set up via setCallback
115    private static RemoteCallbackList<IEmailServiceCallback> sCallbackList =
116        new RemoteCallbackList<IEmailServiceCallback>();
117
118    protected Controller(Context _context) {
119        mContext = _context.getApplicationContext();
120        mProviderContext = _context;
121        mLegacyController = MessagingController.getInstance(mProviderContext, this);
122        mLegacyController.addListener(mLegacyListener);
123    }
124
125    /**
126     * Cleanup for test.  Mustn't be called for the regular {@link Controller}, as it's a
127     * singleton and lives till the process finishes.
128     *
129     * <p>However, this method MUST be called for mock instances.
130     */
131    public void cleanupForTest() {
132        mLegacyController.removeListener(mLegacyListener);
133    }
134
135    /**
136     * Gets or creates the singleton instance of Controller.
137     */
138    public synchronized static Controller getInstance(Context _context) {
139        if (sInstance == null) {
140            sInstance = new Controller(_context);
141        }
142        return sInstance;
143    }
144
145    /**
146     * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
147     *
148     * Tests that use this method MUST clean it up by calling this method again with null.
149     */
150    public synchronized static void injectMockControllerForTest(Controller mockController) {
151        sInstance = mockController;
152    }
153
154    /**
155     * For testing only:  Inject a different context for provider access.  This will be
156     * used internally for access the underlying provider (e.g. getContentResolver().query()).
157     * @param providerContext the provider context to be used by this instance
158     */
159    public void setProviderContext(Context providerContext) {
160        mProviderContext = providerContext;
161    }
162
163    /**
164     * Any UI code that wishes for callback results (on async ops) should register their callback
165     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
166     * problems when the command completes and the activity has already paused or finished.
167     * @param listener The callback that may be used in action methods
168     */
169    public void addResultCallback(Result listener) {
170        synchronized (mListeners) {
171            listener.setRegistered(true);
172            mListeners.add(listener);
173        }
174    }
175
176    /**
177     * Any UI code that no longer wishes for callback results (on async ops) should unregister
178     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
179     * to prevent problems when the command completes and the activity has already paused or
180     * finished.
181     * @param listener The callback that may no longer be used
182     */
183    public void removeResultCallback(Result listener) {
184        synchronized (mListeners) {
185            listener.setRegistered(false);
186            mListeners.remove(listener);
187        }
188    }
189
190    public Collection<Result> getResultCallbacksForTest() {
191        return mListeners;
192    }
193
194    /**
195     * Delete all Messages that live in the attachment mailbox
196     */
197    public void deleteAttachmentMessages() {
198        // Note: There should only be one attachment mailbox at present
199        ContentResolver resolver = mProviderContext.getContentResolver();
200        Cursor c = null;
201        try {
202            c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
203                    WHERE_TYPE_ATTACHMENT, null, null);
204            while (c.moveToNext()) {
205                long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
206                // Must delete attachments BEFORE messages
207                AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0,
208                        mailboxId);
209                resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY,
210                        new String[] {Long.toString(mailboxId)});
211           }
212        } finally {
213            if (c != null) {
214                c.close();
215            }
216        }
217    }
218
219    /**
220     * Get a mailbox based on a sqlite WHERE clause
221     */
222    private Mailbox getGlobalMailboxWhere(String where) {
223        Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI,
224                Mailbox.CONTENT_PROJECTION, where, null, null);
225        try {
226            if (c.moveToFirst()) {
227                Mailbox m = new Mailbox();
228                m.restore(c);
229                return m;
230            }
231        } finally {
232            c.close();
233        }
234        return null;
235    }
236
237    /**
238     * Returns the attachment mailbox (where we store eml attachment Emails), creating one
239     * if necessary
240     * @return the global attachment mailbox
241     */
242    public Mailbox getAttachmentMailbox() {
243        Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT);
244        if (m == null) {
245            m = new Mailbox();
246            m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY;
247            m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID;
248            m.mFlagVisible = false;
249            m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID;
250            m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
251            m.mType = Mailbox.TYPE_ATTACHMENT;
252            m.save(mProviderContext);
253        }
254        return m;
255    }
256
257    /**
258     * Returns the search mailbox for the specified account, creating one if necessary
259     * @return the search mailbox for the passed in account
260     */
261    public Mailbox getSearchMailbox(long accountId) {
262        Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH);
263        if (m == null) {
264            m = new Mailbox();
265            m.mAccountKey = accountId;
266            m.mServerId = SEARCH_MAILBOX_SERVER_ID;
267            m.mFlagVisible = true;
268            m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
269            m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
270            m.mType = Mailbox.TYPE_SEARCH;
271            m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
272            m.save(mProviderContext);
273        }
274        return m;
275    }
276
277    /**
278     * Create a Message from the Uri and store it in the attachment mailbox
279     * @param uri the uri containing message content
280     * @return the Message or null
281     */
282    public Message loadMessageFromUri(Uri uri) {
283        Mailbox mailbox = getAttachmentMailbox();
284        if (mailbox == null) return null;
285        try {
286            InputStream is = mProviderContext.getContentResolver().openInputStream(uri);
287            try {
288                // First, create a Pop3Message from the attachment and then parse it
289                Pop3Message pop3Message = new Pop3Message(
290                        ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null);
291                pop3Message.parse(is);
292                // Now, pull out the header fields
293                Message msg = new Message();
294                LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId);
295                // Commit the message to the local store
296                msg.save(mProviderContext);
297                // Setup the rest of the message and mark it completely loaded
298                mLegacyController.copyOneMessageToProvider(pop3Message, msg,
299                        Message.FLAG_LOADED_COMPLETE, mProviderContext);
300                // Restore the complete message and return it
301                return Message.restoreMessageWithId(mProviderContext, msg.mId);
302            } catch (MessagingException e) {
303            } catch (IOException e) {
304            }
305        } catch (FileNotFoundException e) {
306        }
307        return null;
308    }
309
310    /**
311     * Set logging flags for external sync services
312     *
313     * Generally this should be called by anybody who changes Email.DEBUG
314     */
315    public void serviceLogging(int debugFlags) {
316        IEmailService service = ExchangeUtils.getExchangeService(mContext, mServiceCallback);
317        try {
318            service.setLogging(debugFlags);
319        } catch (RemoteException e) {
320            // TODO Change exception handling to be consistent with however this method
321            // is implemented for other protocols
322            Log.d("setLogging", "RemoteException" + e);
323        }
324    }
325
326    /**
327     * Request a remote update of mailboxes for an account.
328     */
329    public void updateMailboxList(final long accountId) {
330        Utility.runAsync(new Runnable() {
331            @Override
332            public void run() {
333                final IEmailService service = getServiceForAccount(accountId);
334                if (service != null) {
335                    // Service implementation
336                    try {
337                        service.updateFolderList(accountId);
338                    } catch (RemoteException e) {
339                        // TODO Change exception handling to be consistent with however this method
340                        // is implemented for other protocols
341                        Log.d("updateMailboxList", "RemoteException" + e);
342                    }
343                } else {
344                    // MessagingController implementation
345                    mLegacyController.listFolders(accountId, mLegacyListener);
346                }
347            }
348        });
349    }
350
351    /**
352     * Request a remote update of a mailbox.  For use by the timed service.
353     *
354     * Functionally this is quite similar to updateMailbox(), but it's a separate API and
355     * separate callback in order to keep UI callbacks from affecting the service loop.
356     */
357    public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) {
358        IEmailService service = getServiceForAccount(accountId);
359        if (service != null) {
360            // Service implementation
361//            try {
362                // TODO this isn't quite going to work, because we're going to get the
363                // generic (UI) callbacks and not the ones we need to restart the ol' service.
364                // service.startSync(mailboxId, tag);
365            mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag);
366//            } catch (RemoteException e) {
367                // TODO Change exception handling to be consistent with however this method
368                // is implemented for other protocols
369//                Log.d("updateMailbox", "RemoteException" + e);
370//            }
371        } else {
372            // MessagingController implementation
373            Utility.runAsync(new Runnable() {
374                public void run() {
375                    mLegacyController.checkMail(accountId, tag, mLegacyListener);
376                }
377            });
378        }
379    }
380
381    /**
382     * Request a remote update of a mailbox.
383     *
384     * The contract here should be to try and update the headers ASAP, in order to populate
385     * a simple message list.  We should also at this point queue up a background task of
386     * downloading some/all of the messages in this mailbox, but that should be interruptable.
387     */
388    public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) {
389
390        IEmailService service = getServiceForAccount(accountId);
391        if (service != null) {
392            // Service implementation
393            try {
394                service.startSync(mailboxId, userRequest);
395            } catch (RemoteException e) {
396                // TODO Change exception handling to be consistent with however this method
397                // is implemented for other protocols
398                Log.d("updateMailbox", "RemoteException" + e);
399            }
400        } else {
401            // MessagingController implementation
402            Utility.runAsync(new Runnable() {
403                public void run() {
404                    // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
405                    Account account =
406                        EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
407                    Mailbox mailbox =
408                        EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
409                    if (account == null || mailbox == null) {
410                        return;
411                    }
412                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
413                }
414            });
415        }
416    }
417
418    /**
419     * Request that any final work necessary be done, to load a message.
420     *
421     * Note, this assumes that the caller has already checked message.mFlagLoaded and that
422     * additional work is needed.  There is no optimization here for a message which is already
423     * loaded.
424     *
425     * @param messageId the message to load
426     * @param callback the Controller callback by which results will be reported
427     */
428    public void loadMessageForView(final long messageId) {
429
430        // Split here for target type (Service or MessagingController)
431        IEmailService service = getServiceForMessage(messageId);
432        if (service != null) {
433            // There is no service implementation, so we'll just jam the value, log the error,
434            // and get out of here.
435            Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
436            ContentValues cv = new ContentValues();
437            cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
438            mProviderContext.getContentResolver().update(uri, cv, null, null);
439            Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
440            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
441            synchronized (mListeners) {
442                for (Result listener : mListeners) {
443                    listener.loadMessageForViewCallback(null, accountId, messageId, 100);
444                }
445            }
446        } else {
447            // MessagingController implementation
448            Utility.runAsync(new Runnable() {
449                public void run() {
450                    mLegacyController.loadMessageForView(messageId, mLegacyListener);
451                }
452            });
453        }
454    }
455
456
457    /**
458     * Saves the message to a mailbox of given type.
459     * This is a synchronous operation taking place in the same thread as the caller.
460     * Upon return the message.mId is set.
461     * @param message the message (must have the mAccountId set).
462     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
463     */
464    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
465        long accountId = message.mAccountKey;
466        long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
467        message.mMailboxKey = mailboxId;
468        message.save(mProviderContext);
469    }
470
471    /**
472     * Look for a specific mailbox, creating it if necessary, and return the mailbox id.
473     * This is a blocking operation and should not be called from the UI thread.
474     *
475     * Synchronized so multiple threads can call it (and not risk creating duplicate boxes).
476     *
477     * @param accountId the account id
478     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
479     * @return the id of the mailbox. The mailbox is created if not existing.
480     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
481     * Does not validate the input in other ways (e.g. does not verify the existence of account).
482     */
483    public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) {
484        if (accountId < 0 || mailboxType < 0) {
485            return Mailbox.NO_MAILBOX;
486        }
487        long mailboxId =
488            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
489        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
490    }
491
492    /**
493     * Returns the server-side name for a specific mailbox.
494     *
495     * @param mailboxType the mailbox type
496     * @return the resource string corresponding to the mailbox type, empty if not found.
497     */
498    /* package */ String getMailboxServerName(int mailboxType) {
499        int resId = -1;
500        switch (mailboxType) {
501            case Mailbox.TYPE_INBOX:
502                resId = R.string.mailbox_name_server_inbox;
503                break;
504            case Mailbox.TYPE_OUTBOX:
505                resId = R.string.mailbox_name_server_outbox;
506                break;
507            case Mailbox.TYPE_DRAFTS:
508                resId = R.string.mailbox_name_server_drafts;
509                break;
510            case Mailbox.TYPE_TRASH:
511                resId = R.string.mailbox_name_server_trash;
512                break;
513            case Mailbox.TYPE_SENT:
514                resId = R.string.mailbox_name_server_sent;
515                break;
516            case Mailbox.TYPE_JUNK:
517                resId = R.string.mailbox_name_server_junk;
518                break;
519        }
520        return resId != -1 ? mContext.getString(resId) : "";
521    }
522
523    /**
524     * Create a mailbox given the account and mailboxType.
525     * TODO: Does this need to be signaled explicitly to the sync engines?
526     */
527    /* package */ long createMailbox(long accountId, int mailboxType) {
528        if (accountId < 0 || mailboxType < 0) {
529            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
530            Log.e(Logging.LOG_TAG, mes);
531            throw new RuntimeException(mes);
532        }
533        Mailbox box = new Mailbox();
534        box.mAccountKey = accountId;
535        box.mType = mailboxType;
536        box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
537        box.mFlagVisible = true;
538        box.mServerId = box.mDisplayName = getMailboxServerName(mailboxType);
539        box.save(mProviderContext);
540        return box.mId;
541    }
542
543    /**
544     * Send a message:
545     * - move the message to Outbox (the message is assumed to be in Drafts).
546     * - EAS service will take it from there
547     * - trigger send for POP/IMAP
548     * @param messageId the id of the message to send
549     */
550    public void sendMessage(long messageId, long accountId) {
551        ContentResolver resolver = mProviderContext.getContentResolver();
552        if (accountId == -1) {
553            accountId = lookupAccountForMessage(messageId);
554        }
555        if (accountId == -1) {
556            // probably the message was not found
557            if (Email.LOGD) {
558                Email.log("no account found for message " + messageId);
559            }
560            return;
561        }
562
563        // Move to Outbox
564        long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
565        ContentValues cv = new ContentValues();
566        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
567
568        // does this need to be SYNCED_CONTENT_URI instead?
569        Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
570        resolver.update(uri, cv, null, null);
571
572        sendPendingMessages(accountId);
573    }
574
575    private void sendPendingMessagesSmtp(long accountId) {
576        // for IMAP & POP only, (attempt to) send the message now
577        final EmailContent.Account account =
578                EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
579        if (account == null) {
580            return;
581        }
582        final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
583        Utility.runAsync(new Runnable() {
584            public void run() {
585                mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
586            }
587        });
588    }
589
590    /**
591     * Try to send all pending messages for a given account
592     *
593     * @param accountId the account for which to send messages
594     */
595    public void sendPendingMessages(long accountId) {
596        // 1. make sure we even have an outbox, exit early if not
597        final long outboxId =
598            Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
599        if (outboxId == Mailbox.NO_MAILBOX) {
600            return;
601        }
602
603        // 2. dispatch as necessary
604        IEmailService service = getServiceForAccount(accountId);
605        if (service != null) {
606            // Service implementation
607            try {
608                service.startSync(outboxId, false);
609            } catch (RemoteException e) {
610                // TODO Change exception handling to be consistent with however this method
611                // is implemented for other protocols
612                Log.d("updateMailbox", "RemoteException" + e);
613            }
614        } else {
615            // MessagingController implementation
616            sendPendingMessagesSmtp(accountId);
617        }
618    }
619
620    /**
621     * Reset visible limits for all accounts.
622     * For each account:
623     *   look up limit
624     *   write limit into all mailboxes for that account
625     */
626    public void resetVisibleLimits() {
627        Utility.runAsync(new Runnable() {
628            public void run() {
629                ContentResolver resolver = mProviderContext.getContentResolver();
630                Cursor c = null;
631                try {
632                    c = resolver.query(
633                            Account.CONTENT_URI,
634                            Account.ID_PROJECTION,
635                            null, null, null);
636                    while (c.moveToNext()) {
637                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
638                        Account account = Account.restoreAccountWithId(mProviderContext, accountId);
639                        if (account != null) {
640                            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
641                                    account.getStoreUri(mProviderContext), mContext);
642                            if (info != null && info.mVisibleLimitDefault > 0) {
643                                int limit = info.mVisibleLimitDefault;
644                                ContentValues cv = new ContentValues();
645                                cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
646                                resolver.update(Mailbox.CONTENT_URI, cv,
647                                        MailboxColumns.ACCOUNT_KEY + "=?",
648                                        new String[] { Long.toString(accountId) });
649                            }
650                        }
651                    }
652                } finally {
653                    if (c != null) {
654                        c.close();
655                    }
656                }
657            }
658        });
659    }
660
661    /**
662     * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
663     * IMAP and POP.
664     *
665     * @param mailboxId the mailbox
666     */
667    public void loadMoreMessages(final long mailboxId) {
668        Utility.runAsync(new Runnable() {
669            public void run() {
670                Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
671                if (mailbox == null) {
672                    return;
673                }
674                Account account = Account.restoreAccountWithId(mProviderContext,
675                        mailbox.mAccountKey);
676                if (account == null) {
677                    return;
678                }
679                Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
680                        account.getStoreUri(mProviderContext), mContext);
681                if (info != null && info.mVisibleLimitIncrement > 0) {
682                    // Use provider math to increment the field
683                    ContentValues cv = new ContentValues();;
684                    cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
685                    cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement);
686                    Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
687                    mProviderContext.getContentResolver().update(uri, cv, null, null);
688                    // Trigger a refresh using the new, longer limit
689                    mailbox.mVisibleLimit += info.mVisibleLimitIncrement;
690                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
691                }
692            }
693        });
694    }
695
696    /**
697     * @param messageId the id of message
698     * @return the accountId corresponding to the given messageId, or -1 if not found.
699     */
700    private long lookupAccountForMessage(long messageId) {
701        ContentResolver resolver = mProviderContext.getContentResolver();
702        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
703                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
704                                  new String[] { Long.toString(messageId) }, null);
705        try {
706            return c.moveToFirst()
707                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
708                : -1;
709        } finally {
710            c.close();
711        }
712    }
713
714    /**
715     * Delete a single attachment entry from the DB given its id.
716     * Does not delete any eventual associated files.
717     */
718    public void deleteAttachment(long attachmentId) {
719        ContentResolver resolver = mProviderContext.getContentResolver();
720        Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
721        resolver.delete(uri, null, null);
722    }
723
724    /**
725     * Delete a single message by moving it to the trash, or really delete it if it's already in
726     * trash or a draft message.
727     *
728     * This function has no callback, no result reporting, because the desired outcome
729     * is reflected entirely by changes to one or more cursors.
730     *
731     * @param messageId The id of the message to "delete".
732     * @param accountId The id of the message's account, or -1 if not known by caller
733     */
734    public void deleteMessage(final long messageId, final long accountId) {
735        Utility.runAsync(new Runnable() {
736            public void run() {
737                deleteMessageSync(messageId, accountId);
738            }
739        });
740    }
741
742    /**
743     * Synchronous version of {@link #deleteMessage} for tests.
744     */
745    /* package */ void deleteMessageSync(long messageId, long accountId) {
746        // 1. Get the message's account
747        Account account = Account.getAccountForMessageId(mProviderContext, messageId);
748
749        if (account == null) return;
750
751        // 2. Confirm that there is a trash mailbox available.  If not, create one
752        long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH);
753
754        // 3. Get the message's original mailbox
755        Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId);
756
757        if (mailbox == null) return;
758
759        // 4.  Drop non-essential data for the message (e.g. attachment files)
760        AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId,
761                messageId);
762
763        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI,
764                messageId);
765        ContentResolver resolver = mProviderContext.getContentResolver();
766
767        // 5. Perform "delete" as appropriate
768        if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) {
769            // 5a. Really delete it
770            resolver.delete(uri, null, null);
771        } else {
772            // 5b. Move to trash
773            ContentValues cv = new ContentValues();
774            cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
775            resolver.update(uri, cv, null, null);
776        }
777
778        if (isMessagingController(account)) {
779            mLegacyController.processPendingActions(account.mId);
780        }
781    }
782
783    /**
784     * Moves messages to a new mailbox.
785     *
786     * This function has no callback, no result reporting, because the desired outcome
787     * is reflected entirely by changes to one or more cursors.
788     *
789     * Note this method assumes all of the given message and mailbox IDs belong to the same
790     * account.
791     *
792     * @param messageIds IDs of the messages that are to be moved
793     * @param newMailboxId ID of the new mailbox that the messages will be moved to
794     * @return an asynchronous task that executes the move (for testing only)
795     */
796    public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds,
797            final long newMailboxId) {
798        if (messageIds == null || messageIds.length == 0) {
799            throw new InvalidParameterException();
800        }
801        return EmailAsyncTask.runAsyncParallel(new Runnable() {
802            public void run() {
803                Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]);
804                if (account != null) {
805                    ContentValues cv = new ContentValues();
806                    cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId);
807                    ContentResolver resolver = mProviderContext.getContentResolver();
808                    for (long messageId : messageIds) {
809                        Uri uri = ContentUris.withAppendedId(
810                                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
811                        resolver.update(uri, cv, null, null);
812                    }
813                    if (isMessagingController(account)) {
814                        mLegacyController.processPendingActions(account.mId);
815                    }
816                }
817            }
818        });
819    }
820
821    /**
822     * Set/clear the unread status of a message
823     *
824     * @param messageId the message to update
825     * @param isRead the new value for the isRead flag
826     * @return the AsyncTask that will execute the changes (for testing only)
827     */
828    public AsyncTask<Void, Void, Void> setMessageRead(final long messageId, final boolean isRead) {
829        return setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
830    }
831
832    /**
833     * Set/clear the favorite status of a message
834     *
835     * @param messageId the message to update
836     * @param isFavorite the new value for the isFavorite flag
837     * @return the AsyncTask that will execute the changes (for testing only)
838     */
839    public AsyncTask<Void, Void, Void> setMessageFavorite(final long messageId,
840            final boolean isFavorite) {
841        return setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
842    }
843
844    /**
845     * Set/clear boolean columns of a message
846     *
847     * @param messageId the message to update
848     * @param columnName the column to update
849     * @param columnValue the new value for the column
850     * @return the AsyncTask that will execute the changes (for testing only)
851     */
852    private AsyncTask<Void, Void, Void> setMessageBoolean(final long messageId,
853            final String columnName, final boolean columnValue) {
854        return Utility.runAsync(new Runnable() {
855            public void run() {
856                ContentValues cv = new ContentValues();
857                cv.put(columnName, columnValue);
858                Uri uri = ContentUris.withAppendedId(
859                        EmailContent.Message.SYNCED_CONTENT_URI, messageId);
860                mProviderContext.getContentResolver().update(uri, cv, null, null);
861
862                // Service runs automatically, MessagingController needs a kick
863                long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
864                if (accountId == -1) {
865                    return;
866                }
867                if (isMessagingController(accountId)) {
868                    mLegacyController.processPendingActions(accountId);
869                }
870            }
871        });
872    }
873
874    /**
875     * Search for messages on the server; see {@Link EmailServiceProxy#searchMessages(long, long,
876     * boolean, String, int, int, long)} for a complete description of this method's arguments.
877     */
878    public void searchMessages(final long accountId, final long mailboxId,
879            final boolean includeSubfolders, final String query, final int numResults,
880            final int firstResult, final long destMailboxId) {
881        IEmailService service = getServiceForAccount(accountId);
882        if (service != null) {
883            // Service implementation
884            try {
885                service.searchMessages(accountId, mailboxId, includeSubfolders, query, numResults,
886                        firstResult, destMailboxId);
887            } catch (RemoteException e) {
888                // TODO Change exception handling to be consistent with however this method
889                // is implemented for other protocols
890                Log.e("searchMessages", "RemoteException", e);
891            }
892        }
893    }
894
895    /**
896     * Respond to a meeting invitation.
897     *
898     * @param messageId the id of the invitation being responded to
899     * @param response the code representing the response to the invitation
900     */
901    public void sendMeetingResponse(final long messageId, final int response) {
902         // Split here for target type (Service or MessagingController)
903        IEmailService service = getServiceForMessage(messageId);
904        if (service != null) {
905            // Service implementation
906            try {
907                service.sendMeetingResponse(messageId, response);
908            } catch (RemoteException e) {
909                // TODO Change exception handling to be consistent with however this method
910                // is implemented for other protocols
911                Log.e("onDownloadAttachment", "RemoteException", e);
912            }
913        }
914    }
915
916    /**
917     * Request that an attachment be loaded.  It will be stored at a location controlled
918     * by the AttachmentProvider.
919     *
920     * @param attachmentId the attachment to load
921     * @param messageId the owner message
922     * @param accountId the owner account
923     */
924    public void loadAttachment(final long attachmentId, final long messageId,
925            final long accountId) {
926        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
927        if (attachInfo == null) {
928            return;
929        }
930
931        if (Utility.attachmentExists(mProviderContext, attachInfo)) {
932            // The attachment has already been downloaded, so we will just "pretend" to download it
933            // This presumably is for POP3 messages
934            synchronized (mListeners) {
935                for (Result listener : mListeners) {
936                    listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
937                }
938                for (Result listener : mListeners) {
939                    listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
940                }
941            }
942            return;
943        }
944
945        // Flag the attachment as needing download at the user's request
946        ContentValues cv = new ContentValues();
947        cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
948        attachInfo.update(mProviderContext, cv);
949    }
950
951    /**
952     * For a given message id, return a service proxy if applicable, or null.
953     *
954     * @param messageId the message of interest
955     * @result service proxy, or null if n/a
956     */
957    private IEmailService getServiceForMessage(long messageId) {
958        // TODO make this more efficient, caching the account, smaller lookup here, etc.
959        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
960        if (message == null) {
961            return null;
962        }
963        return getServiceForAccount(message.mAccountKey);
964    }
965
966    /**
967     * For a given account id, return a service proxy if applicable, or null.
968     *
969     * @param accountId the message of interest
970     * @result service proxy, or null if n/a
971     */
972    private IEmailService getServiceForAccount(long accountId) {
973        if (isMessagingController(accountId)) return null;
974        return getExchangeEmailService();
975    }
976
977    private IEmailService getExchangeEmailService() {
978        return ExchangeUtils.getExchangeService(mContext, mServiceCallback);
979    }
980
981    /**
982     * Simple helper to determine if legacy MessagingController should be used
983     */
984    public boolean isMessagingController(EmailContent.Account account) {
985        if (account == null) return false;
986        return isMessagingController(account.mId);
987    }
988
989    public boolean isMessagingController(long accountId) {
990        Boolean isLegacyController = mLegacyControllerMap.get(accountId);
991        if (isLegacyController == null) {
992            String protocol = Account.getProtocol(mProviderContext, accountId);
993            isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol));
994            mLegacyControllerMap.put(accountId, isLegacyController);
995        }
996        return isLegacyController;
997    }
998
999    /**
1000     * Delete an account.
1001     */
1002    public void deleteAccount(final long accountId) {
1003        Utility.runAsync(new Runnable() {
1004            public void run() {
1005                deleteAccountSync(accountId, mProviderContext);
1006            }
1007        });
1008    }
1009
1010    /**
1011     * Backup our accounts; define this here so that unit tests can override the behavior
1012     * @param context the caller's context
1013     */
1014    @VisibleForTesting
1015    protected void backupAccounts(Context context) {
1016        AccountBackupRestore.backupAccounts(context);
1017    }
1018
1019    /**
1020     * Delete an account synchronously.
1021     */
1022    public void deleteAccountSync(long accountId, Context context) {
1023        try {
1024            mLegacyControllerMap.remove(accountId);
1025            // Get the account URI.
1026            final Account account = Account.restoreAccountWithId(context, accountId);
1027            if (account == null) {
1028                return; // Already deleted?
1029            }
1030
1031            try {
1032                // Delete Remote store at first.
1033                Store.getInstance(account, context, null).delete();
1034                // Remove the Store instance from cache.
1035                Store.removeInstance(account, context);
1036            } catch (MessagingException e) {
1037                Log.w(Logging.LOG_TAG, "Failed to delete store", e);
1038            }
1039
1040            Uri uri = ContentUris.withAppendedId(
1041                    EmailContent.Account.CONTENT_URI, accountId);
1042            context.getContentResolver().delete(uri, null, null);
1043
1044            backupAccounts(context);
1045
1046            // Release or relax device administration, if relevant
1047            SecurityPolicy.getInstance(context).reducePolicies();
1048
1049            Email.setServicesEnabledSync(context);
1050        } catch (Exception e) {
1051            Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
1052        } finally {
1053            synchronized (mListeners) {
1054                for (Result l : mListeners) {
1055                    l.deleteAccountCallback(accountId);
1056                }
1057            }
1058        }
1059    }
1060
1061    /**
1062     * Delete all synced data, but don't delete the actual account.  This is used when security
1063     * policy requirements are not met, and we don't want to reveal any synced data, but we do
1064     * wish to keep the account configured (e.g. to accept remote wipe commands).
1065     *
1066     * The only mailbox not deleted is the account mailbox (if any)
1067     * Also, clear the sync keys on the remaining account, since the data is gone.
1068     *
1069     * SYNCHRONOUS - do not call from UI thread.
1070     *
1071     * @param accountId The account to wipe.
1072     */
1073    public void deleteSyncedDataSync(long accountId) {
1074        try {
1075            // Delete synced attachments
1076            AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext,
1077                    accountId);
1078
1079            // Delete synced email, leaving only an empty inbox.  We do this in two phases:
1080            // 1. Delete all non-inbox mailboxes (which will delete all of their messages)
1081            // 2. Delete all remaining messages (which will be the inbox messages)
1082            ContentResolver resolver = mProviderContext.getContentResolver();
1083            String[] accountIdArgs = new String[] { Long.toString(accountId) };
1084            resolver.delete(Mailbox.CONTENT_URI,
1085                    MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION,
1086                    accountIdArgs);
1087            resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs);
1088
1089            // Delete sync keys on remaining items
1090            ContentValues cv = new ContentValues();
1091            cv.putNull(Account.SYNC_KEY);
1092            resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
1093            cv.clear();
1094            cv.putNull(Mailbox.SYNC_KEY);
1095            resolver.update(Mailbox.CONTENT_URI, cv,
1096                    MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
1097
1098            // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
1099            IEmailService service = getServiceForAccount(accountId);
1100            if (service != null) {
1101                service.deleteAccountPIMData(accountId);
1102            }
1103        } catch (Exception e) {
1104            Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e);
1105        }
1106    }
1107
1108    /**
1109     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
1110     * and the result is observed via provider cursors.  The callback will *not* necessarily be
1111     * made from the UI thread, so you may need further handlers to safely make UI updates.
1112     */
1113    public static abstract class Result {
1114        private volatile boolean mRegistered;
1115
1116        protected void setRegistered(boolean registered) {
1117            mRegistered = registered;
1118        }
1119
1120        protected final boolean isRegistered() {
1121            return mRegistered;
1122        }
1123
1124        /**
1125         * Callback for updateMailboxList
1126         *
1127         * @param result If null, the operation completed without error
1128         * @param accountId The account being operated on
1129         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1130         */
1131        public void updateMailboxListCallback(MessagingException result, long accountId,
1132                int progress) {
1133        }
1134
1135        /**
1136         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
1137         * it's a separate call used only by UI's, so we can keep things separate.
1138         *
1139         * @param result If null, the operation completed without error
1140         * @param accountId The account being operated on
1141         * @param mailboxId The mailbox being operated on
1142         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1143         * @param numNewMessages the number of new messages delivered
1144         */
1145        public void updateMailboxCallback(MessagingException result, long accountId,
1146                long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) {
1147        }
1148
1149        /**
1150         * Callback for loadMessageForView
1151         *
1152         * @param result if null, the attachment completed - if non-null, terminating with failure
1153         * @param messageId the message which contains the attachment
1154         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1155         */
1156        public void loadMessageForViewCallback(MessagingException result, long accountId,
1157                long messageId, int progress) {
1158        }
1159
1160        /**
1161         * Callback for loadAttachment
1162         *
1163         * @param result if null, the attachment completed - if non-null, terminating with failure
1164         * @param messageId the message which contains the attachment
1165         * @param attachmentId the attachment being loaded
1166         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1167         */
1168        public void loadAttachmentCallback(MessagingException result, long accountId,
1169                long messageId, long attachmentId, int progress) {
1170        }
1171
1172        /**
1173         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
1174         * it's a separate call used only by the automatic checker service, so we can keep
1175         * things separate.
1176         *
1177         * @param result If null, the operation completed without error
1178         * @param accountId The account being operated on
1179         * @param mailboxId The mailbox being operated on (may be unknown at start)
1180         * @param progress 0 for "starting", no updates, 100 for complete
1181         * @param tag the same tag that was passed to serviceCheckMail()
1182         */
1183        public void serviceCheckMailCallback(MessagingException result, long accountId,
1184                long mailboxId, int progress, long tag) {
1185        }
1186
1187        /**
1188         * Callback for sending pending messages.  This will be called once to start the
1189         * group, multiple times for messages, and once to complete the group.
1190         *
1191         * Unfortunately this callback works differently on SMTP and EAS.
1192         *
1193         * On SMTP:
1194         *
1195         * First, we get this.
1196         *  result == null, messageId == -1, progress == 0:     start batch send
1197         *
1198         * Then we get these callbacks per message.
1199         * (Exchange backend may skip "start sending one message".)
1200         *  result == null, messageId == xx, progress == 0:     start sending one message
1201         *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
1202         *
1203         * Finally we get this.
1204         *  result == null, messageId == -1, progres == 100;    finish sending batch
1205         *
1206         * On EAS: Almost same as above, except:
1207         *
1208         * - There's no first ("start batch send") callback.
1209         * - accountId is always -1.
1210         *
1211         * @param result If null, the operation completed without error
1212         * @param accountId The account being operated on
1213         * @param messageId The being sent (may be unknown at start)
1214         * @param progress 0 for "starting", 100 for complete
1215         */
1216        public void sendMailCallback(MessagingException result, long accountId,
1217                long messageId, int progress) {
1218        }
1219
1220        /**
1221         * Callback from {@link Controller#deleteAccount}.
1222         */
1223        public void deleteAccountCallback(long accountId) {
1224        }
1225    }
1226
1227    /**
1228     * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
1229     * pass down to {@link Result}.
1230     */
1231    public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
1232        private final long mMessageId;
1233        private final long mAttachmentId;
1234        private final long mAccountId;
1235
1236        public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
1237            mMessageId = messageId;
1238            mAttachmentId = attachmentId;
1239            mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId);
1240        }
1241
1242        @Override
1243        public void loadAttachmentProgress(int progress) {
1244              synchronized (mListeners) {
1245                  for (Result listener : mListeners) {
1246                      listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId,
1247                              progress);
1248                 }
1249              }
1250        }
1251
1252        @Override
1253        public void messageRetrieved(com.android.emailcommon.mail.Message message) {
1254        }
1255    }
1256
1257    /**
1258     * Support for receiving callbacks from MessagingController and dealing with UI going
1259     * out of scope.
1260     */
1261    public class LegacyListener extends MessagingListener {
1262        public LegacyListener() {
1263        }
1264
1265        @Override
1266        public void listFoldersStarted(long accountId) {
1267            synchronized (mListeners) {
1268                for (Result l : mListeners) {
1269                    l.updateMailboxListCallback(null, accountId, 0);
1270                }
1271            }
1272        }
1273
1274        @Override
1275        public void listFoldersFailed(long accountId, String message) {
1276            synchronized (mListeners) {
1277                for (Result l : mListeners) {
1278                    l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
1279                }
1280            }
1281        }
1282
1283        @Override
1284        public void listFoldersFinished(long accountId) {
1285            synchronized (mListeners) {
1286                for (Result l : mListeners) {
1287                    l.updateMailboxListCallback(null, accountId, 100);
1288                }
1289            }
1290        }
1291
1292        @Override
1293        public void synchronizeMailboxStarted(long accountId, long mailboxId) {
1294            synchronized (mListeners) {
1295                for (Result l : mListeners) {
1296                    l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null);
1297                }
1298            }
1299        }
1300
1301        @Override
1302        public void synchronizeMailboxFinished(long accountId, long mailboxId,
1303                int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
1304            synchronized (mListeners) {
1305                for (Result l : mListeners) {
1306                    l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages,
1307                            addedMessages);
1308                }
1309            }
1310        }
1311
1312        @Override
1313        public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
1314            MessagingException me;
1315            if (e instanceof MessagingException) {
1316                me = (MessagingException) e;
1317            } else {
1318                me = new MessagingException(e.toString());
1319            }
1320            synchronized (mListeners) {
1321                for (Result l : mListeners) {
1322                    l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null);
1323                }
1324            }
1325        }
1326
1327        @Override
1328        public void checkMailStarted(Context context, long accountId, long tag) {
1329            synchronized (mListeners) {
1330                for (Result l : mListeners) {
1331                    l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
1332                }
1333            }
1334        }
1335
1336        @Override
1337        public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
1338            synchronized (mListeners) {
1339                for (Result l : mListeners) {
1340                    l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
1341                }
1342            }
1343        }
1344
1345        @Override
1346        public void loadMessageForViewStarted(long messageId) {
1347            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1348            synchronized (mListeners) {
1349                for (Result listener : mListeners) {
1350                    listener.loadMessageForViewCallback(null, accountId, messageId, 0);
1351                }
1352            }
1353        }
1354
1355        @Override
1356        public void loadMessageForViewFinished(long messageId) {
1357            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1358            synchronized (mListeners) {
1359                for (Result listener : mListeners) {
1360                    listener.loadMessageForViewCallback(null, accountId, messageId, 100);
1361                }
1362            }
1363        }
1364
1365        @Override
1366        public void loadMessageForViewFailed(long messageId, String message) {
1367            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1368            synchronized (mListeners) {
1369                for (Result listener : mListeners) {
1370                    listener.loadMessageForViewCallback(new MessagingException(message),
1371                            accountId, messageId, 0);
1372                }
1373            }
1374        }
1375
1376        @Override
1377        public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
1378                boolean requiresDownload) {
1379            try {
1380                mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
1381                        EmailServiceStatus.IN_PROGRESS, 0);
1382            } catch (RemoteException e) {
1383            }
1384            synchronized (mListeners) {
1385                for (Result listener : mListeners) {
1386                    listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
1387                }
1388            }
1389        }
1390
1391        @Override
1392        public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
1393            try {
1394                mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
1395                        EmailServiceStatus.SUCCESS, 100);
1396            } catch (RemoteException e) {
1397            }
1398            synchronized (mListeners) {
1399                for (Result listener : mListeners) {
1400                    listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
1401                }
1402            }
1403        }
1404
1405        @Override
1406        public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
1407                MessagingException me, boolean background) {
1408            try {
1409                // If the cause of the MessagingException is an IOException, we send a status of
1410                // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to
1411                // download the attachment.  Otherwise, the error is considered non-recoverable.
1412                int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND;
1413                if (me != null && me.getCause() instanceof IOException) {
1414                    status = EmailServiceStatus.CONNECTION_ERROR;
1415                }
1416                mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0);
1417            } catch (RemoteException e) {
1418            }
1419            synchronized (mListeners) {
1420                for (Result listener : mListeners) {
1421                    // TODO We are overloading the exception here. The UI listens for this
1422                    // callback and displays a toast if the exception is not null. Since we
1423                    // want to avoid displaying toast for background operations, we force
1424                    // the exception to be null. This needs to be re-worked so the UI will
1425                    // only receive (or at least pays attention to) responses for requests
1426                    // it explicitly cares about. Then we would not need to overload the
1427                    // exception parameter.
1428                    listener.loadAttachmentCallback(background ? null : me, accountId, messageId,
1429                            attachmentId, 0);
1430                }
1431            }
1432        }
1433
1434        @Override
1435        synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
1436            synchronized (mListeners) {
1437                for (Result listener : mListeners) {
1438                    listener.sendMailCallback(null, accountId, messageId, 0);
1439                }
1440            }
1441        }
1442
1443        @Override
1444        synchronized public void sendPendingMessagesCompleted(long accountId) {
1445            synchronized (mListeners) {
1446                for (Result listener : mListeners) {
1447                    listener.sendMailCallback(null, accountId, -1, 100);
1448                }
1449            }
1450        }
1451
1452        @Override
1453        synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
1454                Exception reason) {
1455            MessagingException me;
1456            if (reason instanceof MessagingException) {
1457                me = (MessagingException) reason;
1458            } else {
1459                me = new MessagingException(reason.toString());
1460            }
1461            synchronized (mListeners) {
1462                for (Result listener : mListeners) {
1463                    listener.sendMailCallback(me, accountId, messageId, 0);
1464                }
1465            }
1466        }
1467    }
1468
1469    /**
1470     * Service callback for service operations
1471     */
1472    private class ServiceCallback extends IEmailServiceCallback.Stub {
1473
1474        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1475
1476        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1477                int progress) {
1478            MessagingException result = mapStatusToException(statusCode);
1479            switch (statusCode) {
1480                case EmailServiceStatus.SUCCESS:
1481                    progress = 100;
1482                    break;
1483                case EmailServiceStatus.IN_PROGRESS:
1484                    if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1485                        result = new MessagingException(
1486                                String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1487                    }
1488                    // discard progress reports that look like sentinels
1489                    if (progress < 0 || progress >= 100) {
1490                        return;
1491                    }
1492                    break;
1493            }
1494            final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
1495            synchronized (mListeners) {
1496                for (Result listener : mListeners) {
1497                    listener.loadAttachmentCallback(result, accountId, messageId, attachmentId,
1498                            progress);
1499                }
1500            }
1501        }
1502
1503        /**
1504         * Note, this is an incomplete implementation of this callback, because we are
1505         * not getting things back from Service in quite the same way as from MessagingController.
1506         * However, this is sufficient for basic "progress=100" notification that message send
1507         * has just completed.
1508         */
1509        public void sendMessageStatus(long messageId, String subject, int statusCode,
1510                int progress) {
1511            long accountId = -1;        // This should be in the callback
1512            MessagingException result = mapStatusToException(statusCode);
1513            switch (statusCode) {
1514                case EmailServiceStatus.SUCCESS:
1515                    progress = 100;
1516                    break;
1517                case EmailServiceStatus.IN_PROGRESS:
1518                    // discard progress reports that look like sentinels
1519                    if (progress < 0 || progress >= 100) {
1520                        return;
1521                    }
1522                    break;
1523            }
1524            synchronized(mListeners) {
1525                for (Result listener : mListeners) {
1526                    listener.sendMailCallback(result, accountId, messageId, progress);
1527                }
1528            }
1529        }
1530
1531        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1532            MessagingException result = mapStatusToException(statusCode);
1533            switch (statusCode) {
1534                case EmailServiceStatus.SUCCESS:
1535                    progress = 100;
1536                    break;
1537                case EmailServiceStatus.IN_PROGRESS:
1538                    // discard progress reports that look like sentinels
1539                    if (progress < 0 || progress >= 100) {
1540                        return;
1541                    }
1542                    break;
1543            }
1544            synchronized(mListeners) {
1545                for (Result listener : mListeners) {
1546                    listener.updateMailboxListCallback(result, accountId, progress);
1547                }
1548            }
1549        }
1550
1551        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1552            MessagingException result = mapStatusToException(statusCode);
1553            switch (statusCode) {
1554                case EmailServiceStatus.SUCCESS:
1555                    progress = 100;
1556                    break;
1557                case EmailServiceStatus.IN_PROGRESS:
1558                    // discard progress reports that look like sentinels
1559                    if (progress < 0 || progress >= 100) {
1560                        return;
1561                    }
1562                    break;
1563            }
1564            // TODO should pass this back instead of looking it up here
1565            Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1566            // The mailbox could have disappeared if the server commanded it
1567            if (mbx == null) return;
1568            long accountId = mbx.mAccountKey;
1569            synchronized(mListeners) {
1570                for (Result listener : mListeners) {
1571                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null);
1572                }
1573            }
1574        }
1575
1576        private MessagingException mapStatusToException(int statusCode) {
1577            switch (statusCode) {
1578                case EmailServiceStatus.SUCCESS:
1579                case EmailServiceStatus.IN_PROGRESS:
1580                // Don't generate error if the account is uninitialized
1581                case EmailServiceStatus.ACCOUNT_UNINITIALIZED:
1582                    return null;
1583
1584                case EmailServiceStatus.LOGIN_FAILED:
1585                    return new AuthenticationFailedException("");
1586
1587                case EmailServiceStatus.CONNECTION_ERROR:
1588                    return new MessagingException(MessagingException.IOERROR);
1589
1590                case EmailServiceStatus.SECURITY_FAILURE:
1591                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1592
1593                case EmailServiceStatus.ACCESS_DENIED:
1594                    return new MessagingException(MessagingException.ACCESS_DENIED);
1595
1596                case EmailServiceStatus.MESSAGE_NOT_FOUND:
1597                case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1598                case EmailServiceStatus.FOLDER_NOT_DELETED:
1599                case EmailServiceStatus.FOLDER_NOT_RENAMED:
1600                case EmailServiceStatus.FOLDER_NOT_CREATED:
1601                case EmailServiceStatus.REMOTE_EXCEPTION:
1602                    // TODO: define exception code(s) & UI string(s) for server-side errors
1603                default:
1604                    return new MessagingException(String.valueOf(statusCode));
1605            }
1606        }
1607    }
1608
1609    private interface ServiceCallbackWrapper {
1610        public void call(IEmailServiceCallback cb) throws RemoteException;
1611    }
1612
1613    /**
1614     * Proxy that can be used to broadcast service callbacks; we currently use this only for
1615     * loadAttachment callbacks
1616     */
1617    private final IEmailServiceCallback.Stub mCallbackProxy =
1618        new IEmailServiceCallback.Stub() {
1619
1620        /**
1621         * Broadcast a callback to the everyone that's registered
1622         *
1623         * @param wrapper the ServiceCallbackWrapper used in the broadcast
1624         */
1625        private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
1626            if (sCallbackList != null) {
1627                // Call everyone on our callback list
1628                // Exceptions can be safely ignored
1629                int count = sCallbackList.beginBroadcast();
1630                for (int i = 0; i < count; i++) {
1631                    try {
1632                        wrapper.call(sCallbackList.getBroadcastItem(i));
1633                    } catch (RemoteException e) {
1634                    }
1635                }
1636                sCallbackList.finishBroadcast();
1637            }
1638        }
1639
1640        public void loadAttachmentStatus(final long messageId, final long attachmentId,
1641                final int status, final int progress) {
1642            broadcastCallback(new ServiceCallbackWrapper() {
1643                @Override
1644                public void call(IEmailServiceCallback cb) throws RemoteException {
1645                    cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
1646                }
1647            });
1648        }
1649
1650        @Override
1651        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){
1652        }
1653
1654        @Override
1655        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1656        }
1657
1658        @Override
1659        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1660        }
1661    };
1662
1663    public static class ControllerService extends Service {
1664        /**
1665         * Create our EmailService implementation here.  For now, only loadAttachment is supported;
1666         * the intention, however, is to move more functionality to the service interface
1667         */
1668        private final IEmailService.Stub mBinder = new IEmailService.Stub() {
1669
1670            public Bundle validate(String protocol, String host, String userName, String password,
1671                    int port, boolean ssl, boolean trustCertificates) {
1672                return null;
1673            }
1674
1675            public Bundle autoDiscover(String userName, String password) {
1676                return null;
1677            }
1678
1679            public void startSync(long mailboxId, boolean userRequest) {
1680            }
1681
1682            public void stopSync(long mailboxId) {
1683            }
1684
1685            public void loadAttachment(long attachmentId, boolean background)
1686                    throws RemoteException {
1687                Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this,
1688                        attachmentId);
1689                if (att != null) {
1690                    if (Email.DEBUG) {
1691                        Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName);
1692                    }
1693                    Message msg = Message.restoreMessageWithId(ControllerService.this,
1694                            att.mMessageKey);
1695                    if (msg != null) {
1696                        // If the message is a forward and the attachment needs downloading, we need
1697                        // to retrieve the message from the source, rather than from the message
1698                        // itself
1699                        if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
1700                            String[] cols = Utility.getRowColumns(ControllerService.this,
1701                                    Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY,
1702                                    new String[] {Long.toString(msg.mId)});
1703                            if (cols != null) {
1704                                msg = Message.restoreMessageWithId(ControllerService.this,
1705                                        Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN]));
1706                                if (msg == null) {
1707                                    // TODO: We can try restoring from the deleted table here...
1708                                    return;
1709                                }
1710                            }
1711                        }
1712                        MessagingController legacyController = sInstance.mLegacyController;
1713                        LegacyListener legacyListener = sInstance.mLegacyListener;
1714                        legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey,
1715                                attachmentId, legacyListener, background);
1716                    } else {
1717                        // Send back the specific error status for this case
1718                        sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId,
1719                                EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
1720                    }
1721                }
1722            }
1723
1724            public void updateFolderList(long accountId) {
1725            }
1726
1727            public void hostChanged(long accountId) {
1728            }
1729
1730            public void setLogging(int flags) {
1731            }
1732
1733            public void sendMeetingResponse(long messageId, int response) {
1734            }
1735
1736            public void loadMore(long messageId) {
1737            }
1738
1739            // The following three methods are not implemented in this version
1740            public boolean createFolder(long accountId, String name) {
1741                return false;
1742            }
1743
1744            public boolean deleteFolder(long accountId, String name) {
1745                return false;
1746            }
1747
1748            public boolean renameFolder(long accountId, String oldName, String newName) {
1749                return false;
1750            }
1751
1752            public void setCallback(IEmailServiceCallback cb) {
1753                sCallbackList.register(cb);
1754            }
1755
1756            public void deleteAccountPIMData(long accountId) {
1757            }
1758
1759            public int searchMessages(long accountId, long mailboxId, boolean includeSubfolders,
1760                    String query, int numResults, int firstResult, long destMailboxId) {
1761                return 0;
1762            }
1763
1764            @Override
1765            public int getApiLevel() {
1766                return Api.LEVEL;
1767            }
1768        };
1769
1770        @Override
1771        public IBinder onBind(Intent intent) {
1772            return mBinder;
1773        }
1774    }
1775}
1776