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