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