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