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