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