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