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