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