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