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