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