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