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