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