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