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