Controller.java revision 6c21942ec45f561d711b3d74ecca8e62afb735c4
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.MessagingException;
20import com.android.email.mail.Store;
21import com.android.email.provider.AttachmentProvider;
22import com.android.email.provider.EmailContent;
23import com.android.email.provider.EmailContent.Account;
24import com.android.email.provider.EmailContent.Attachment;
25import com.android.email.provider.EmailContent.Mailbox;
26import com.android.email.provider.EmailContent.MailboxColumns;
27import com.android.email.provider.EmailContent.Message;
28import com.android.email.provider.EmailContent.MessageColumns;
29import com.android.email.service.EmailServiceProxy;
30import com.android.exchange.EmailServiceStatus;
31import com.android.exchange.IEmailService;
32import com.android.exchange.IEmailServiceCallback;
33import com.android.exchange.SyncManager;
34
35import android.content.ContentResolver;
36import android.content.ContentUris;
37import android.content.ContentValues;
38import android.content.Context;
39import android.database.Cursor;
40import android.net.Uri;
41import android.os.RemoteException;
42import android.util.Log;
43
44import java.io.File;
45import java.util.HashSet;
46
47/**
48 * New central controller/dispatcher for Email activities that may require remote operations.
49 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
50 * based code.
51 */
52public class Controller {
53
54    static Controller sInstance;
55    private Context mContext;
56    private Context mProviderContext;
57    private MessagingController mLegacyController;
58    private LegacyListener mLegacyListener = new LegacyListener();
59    private ServiceCallback mServiceCallback = new ServiceCallback();
60    private HashSet<Result> mListeners = new HashSet<Result>();
61
62    private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
63        EmailContent.RECORD_ID,
64        EmailContent.MessageColumns.ACCOUNT_KEY
65    };
66    private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
67
68    protected Controller(Context _context) {
69        mContext = _context;
70        mProviderContext = _context;
71        mLegacyController = MessagingController.getInstance(mContext);
72        mLegacyController.addListener(mLegacyListener);
73    }
74
75    /**
76     * Gets or creates the singleton instance of Controller.
77     * @param _context The context that will be used for all underlying system access
78     */
79    public synchronized static Controller getInstance(Context _context) {
80        if (sInstance == null) {
81            sInstance = new Controller(_context);
82        }
83        return sInstance;
84    }
85
86    /**
87     * For testing only:  Inject a different context for provider access.  This will be
88     * used internally for access the underlying provider (e.g. getContentResolver().query()).
89     * @param providerContext the provider context to be used by this instance
90     */
91    public void setProviderContext(Context providerContext) {
92        mProviderContext = providerContext;
93    }
94
95    /**
96     * Any UI code that wishes for callback results (on async ops) should register their callback
97     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
98     * problems when the command completes and the activity has already paused or finished.
99     * @param listener The callback that may be used in action methods
100     */
101    public void addResultCallback(Result listener) {
102        synchronized (mListeners) {
103            mListeners.add(listener);
104        }
105    }
106
107    /**
108     * Any UI code that no longer wishes for callback results (on async ops) should unregister
109     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
110     * to prevent problems when the command completes and the activity has already paused or
111     * finished.
112     * @param listener The callback that may no longer be used
113     */
114    public void removeResultCallback(Result listener) {
115        synchronized (mListeners) {
116            mListeners.remove(listener);
117        }
118    }
119
120    private boolean isActiveResultCallback(Result listener) {
121        synchronized (mListeners) {
122            return mListeners.contains(listener);
123        }
124    }
125
126    /**
127     * Enable/disable logging for external sync services
128     *
129     * Generally this should be called by anybody who changes Email.DEBUG
130     */
131    public void serviceLogging(int debugEnabled) {
132        IEmailService service =
133            new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback);
134        try {
135            service.setLogging(debugEnabled);
136        } catch (RemoteException e) {
137            // TODO Change exception handling to be consistent with however this method
138            // is implemented for other protocols
139            Log.d("updateMailboxList", "RemoteException" + e);
140        }
141    }
142
143    /**
144     * Request a remote update of mailboxes for an account.
145     *
146     * TODO: Clean up threading in MessagingController cases (or perhaps here in Controller)
147     */
148    public void updateMailboxList(final long accountId, final Result callback) {
149
150        IEmailService service = getServiceForAccount(accountId);
151        if (service != null) {
152            // Service implementation
153            try {
154                service.updateFolderList(accountId);
155            } catch (RemoteException e) {
156                // TODO Change exception handling to be consistent with however this method
157                // is implemented for other protocols
158                Log.d("updateMailboxList", "RemoteException" + e);
159            }
160        } else {
161            // MessagingController implementation
162            new Thread() {
163                @Override
164                public void run() {
165                    mLegacyController.listFolders(accountId, mLegacyListener);
166                }
167            }.start();
168        }
169    }
170
171    /**
172     * Request a remote update of a mailbox.  For use by the timed service.
173     *
174     * Functionally this is quite similar to updateMailbox(), but it's a separate API and
175     * separate callback in order to keep UI callbacks from affecting the service loop.
176     */
177    public void serviceCheckMail(final long accountId, final long mailboxId, final long tag,
178            final Result callback) {
179        IEmailService service = getServiceForAccount(accountId);
180        if (service != null) {
181            // Service implementation
182//            try {
183                // TODO this isn't quite going to work, because we're going to get the
184                // generic (UI) callbacks and not the ones we need to restart the ol' service.
185                // service.startSync(mailboxId, tag);
186                callback.serviceCheckMailCallback(null, accountId, mailboxId, 100, tag);
187//            } catch (RemoteException e) {
188                // TODO Change exception handling to be consistent with however this method
189                // is implemented for other protocols
190//                Log.d("updateMailbox", "RemoteException" + e);
191//            }
192        } else {
193            // MessagingController implementation
194            new Thread() {
195                @Override
196                public void run() {
197                    mLegacyController.checkMail(accountId, tag, mLegacyListener);
198                }
199            }.start();
200        }
201    }
202
203    /**
204     * Request a remote update of a mailbox.
205     *
206     * The contract here should be to try and update the headers ASAP, in order to populate
207     * a simple message list.  We should also at this point queue up a background task of
208     * downloading some/all of the messages in this mailbox, but that should be interruptable.
209     */
210    public void updateMailbox(final long accountId, final long mailboxId, final Result callback) {
211
212        IEmailService service = getServiceForAccount(accountId);
213        if (service != null) {
214            // Service implementation
215            try {
216                service.startSync(mailboxId);
217            } catch (RemoteException e) {
218                // TODO Change exception handling to be consistent with however this method
219                // is implemented for other protocols
220                Log.d("updateMailbox", "RemoteException" + e);
221            }
222        } else {
223            // MessagingController implementation
224            new Thread() {
225                @Override
226                public void run() {
227                    // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
228                    Account account =
229                        EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
230                    Mailbox mailbox =
231                        EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
232                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
233                }
234            }.start();
235        }
236    }
237
238    /**
239     * Request that any final work necessary be done, to load a message.
240     *
241     * Note, this assumes that the caller has already checked message.mFlagLoaded and that
242     * additional work is needed.  There is no optimization here for a message which is already
243     * loaded.
244     *
245     * @param messageId the message to load
246     * @param callback the Controller callback by which results will be reported
247     */
248    public void loadMessageForView(final long messageId, final Result callback) {
249
250        // Split here for target type (Service or MessagingController)
251        IEmailService service = getServiceForMessage(messageId);
252        if (service != null) {
253            // There is no service implementation, so we'll just jam the value, log the error,
254            // and get out of here.
255            Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
256            ContentValues cv = new ContentValues();
257            cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
258            mContext.getContentResolver().update(uri, cv, null, null);
259            Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
260            synchronized (mListeners) {
261                for (Result listener : mListeners) {
262                    listener.loadMessageForViewCallback(null, messageId, 100);
263                }
264            }
265        } else {
266            // MessagingController implementation
267            new Thread() {
268                @Override
269                public void run() {
270                    mLegacyController.loadMessageForView(messageId, mLegacyListener);
271                }
272            }.start();
273        }
274    }
275
276
277    /**
278     * Saves the message to a mailbox of given type.
279     * This is a synchronous operation taking place in the same thread as the caller.
280     * Upon return the message.mId is set.
281     * @param message the message (must have the mAccountId set).
282     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
283     */
284    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
285        long accountId = message.mAccountKey;
286        long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
287        message.mMailboxKey = mailboxId;
288        message.save(mContext);
289    }
290
291    /**
292     * @param accountId the account id
293     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
294     * @return the id of the mailbox. The mailbox is created if not existing.
295     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
296     * Does not validate the input in other ways (e.g. does not verify the existence of account).
297     */
298    public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
299        if (accountId < 0 || mailboxType < 0) {
300            return Mailbox.NO_MAILBOX;
301        }
302        long mailboxId =
303            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
304        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
305    }
306
307    /**
308     * @param mailboxType the mailbox type
309     * @return the resource string corresponding to the mailbox type, empty if not found.
310     */
311    /* package */ String getSpecialMailboxDisplayName(int mailboxType) {
312        int resId = -1;
313        switch (mailboxType) {
314            case Mailbox.TYPE_INBOX:
315                // TODO: there is no special_mailbox_display_name_inbox; why?
316                resId = R.string.special_mailbox_name_inbox;
317                break;
318            case Mailbox.TYPE_OUTBOX:
319                resId = R.string.special_mailbox_display_name_outbox;
320                break;
321            case Mailbox.TYPE_DRAFTS:
322                resId = R.string.special_mailbox_display_name_drafts;
323                break;
324            case Mailbox.TYPE_TRASH:
325                resId = R.string.special_mailbox_display_name_trash;
326                break;
327            case Mailbox.TYPE_SENT:
328                resId = R.string.special_mailbox_display_name_sent;
329                break;
330        }
331        return resId != -1 ? mContext.getString(resId) : "";
332    }
333
334    /**
335     * Create a mailbox given the account and mailboxType.
336     * TODO: Does this need to be signaled explicitly to the sync engines?
337     * As this method is only used internally ('private'), it does not
338     * validate its inputs (accountId and mailboxType).
339     */
340    /* package */ long createMailbox(long accountId, int mailboxType) {
341        if (accountId < 0 || mailboxType < 0) {
342            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
343            Log.e(Email.LOG_TAG, mes);
344            throw new RuntimeException(mes);
345        }
346        Mailbox box = new Mailbox();
347        box.mAccountKey = accountId;
348        box.mType = mailboxType;
349        box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
350        box.mFlagVisible = true;
351        box.mDisplayName = getSpecialMailboxDisplayName(mailboxType);
352        box.save(mProviderContext);
353        return box.mId;
354    }
355
356    /**
357     * Send a message:
358     * - move the message to Outbox (the message is assumed to be in Drafts).
359     * - EAS service will take it from there
360     * - trigger send for POP/IMAP
361     * @param messageId the id of the message to send
362     */
363    public void sendMessage(long messageId, long accountId) {
364        ContentResolver resolver = mProviderContext.getContentResolver();
365        if (accountId == -1) {
366            accountId = lookupAccountForMessage(messageId);
367        }
368        if (accountId == -1) {
369            // probably the message was not found
370            if (Email.LOGD) {
371                Email.log("no account found for message " + messageId);
372            }
373            return;
374        }
375
376        // Move to Outbox
377        long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
378        ContentValues cv = new ContentValues();
379        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
380
381        // does this need to be SYNCED_CONTENT_URI instead?
382        Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
383        resolver.update(uri, cv, null, null);
384
385        // for IMAP & POP only, (attempt to) send the message now
386        final EmailContent.Account account =
387                EmailContent.Account.restoreAccountWithId(mContext, accountId);
388        if (this.isMessagingController(account)) {
389            final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
390            new Thread() {
391                @Override
392                public void run() {
393                    mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
394                }
395            }.start();
396        }
397    }
398
399    /**
400     * Try to send all pending messages for a given account
401     *
402     * @param accountId the account for which to send messages (-1 for all accounts)
403     * @param callback
404     */
405    public void sendPendingMessages(long accountId, Result callback) {
406        // 1. make sure we even have an outbox, exit early if not
407        final long outboxId =
408            Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
409        if (outboxId == Mailbox.NO_MAILBOX) {
410            return;
411        }
412
413        // 2. dispatch as necessary
414        IEmailService service = getServiceForAccount(accountId);
415        if (service != null) {
416            // Service implementation
417            try {
418                service.startSync(outboxId);
419            } catch (RemoteException e) {
420                // TODO Change exception handling to be consistent with however this method
421                // is implemented for other protocols
422                Log.d("updateMailbox", "RemoteException" + e);
423            }
424        } else {
425            // MessagingController implementation
426            final EmailContent.Account account =
427                EmailContent.Account.restoreAccountWithId(mContext, accountId);
428            final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
429            new Thread() {
430                @Override
431                public void run() {
432                    mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
433                }
434            }.start();
435        }
436    }
437
438    /**
439     * Reset visible limits for all accounts.
440     * For each account:
441     *   look up limit
442     *   write limit into all mailboxes for that account
443     */
444    public void resetVisibleLimits() {
445        new Thread() {
446            @Override
447            public void run() {
448                ContentResolver resolver = mContext.getContentResolver();
449                Cursor c = null;
450                try {
451                    c = resolver.query(
452                            Account.CONTENT_URI,
453                            Account.ID_PROJECTION,
454                            null, null, null);
455                    while (c.moveToNext()) {
456                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
457                        Account account = Account.restoreAccountWithId(mContext, accountId);
458                        if (account != null) {
459                            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
460                                    account.getStoreUri(mContext), mContext);
461                            if (info != null && info.mVisibleLimitDefault > 0) {
462                                int limit = info.mVisibleLimitDefault;
463                                ContentValues cv = new ContentValues();
464                                cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
465                                resolver.update(Mailbox.CONTENT_URI, cv,
466                                        MailboxColumns.ACCOUNT_KEY + "=?",
467                                        new String[] { Long.toString(accountId) });
468                            }
469                        }
470                    }
471                } finally {
472                    c.close();
473                }
474            }
475        }.start();
476    }
477
478    /**
479     * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
480     * IMAP and POP.
481     *
482     * @param mailboxId the mailbox
483     * @param callback
484     */
485    public void loadMoreMessages(final long mailboxId, Result callback) {
486        new Thread() {
487            @Override
488            public void run() {
489                Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
490                if (mailbox == null) {
491                    return;
492                }
493                Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
494                if (account == null) {
495                    return;
496                }
497                Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
498                        account.getStoreUri(mContext), mContext);
499                if (info != null && info.mVisibleLimitIncrement > 0) {
500                    // Use provider math to increment the field
501                    ContentValues cv = new ContentValues();;
502                    cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
503                    cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement);
504                    Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
505                    mContext.getContentResolver().update(uri, cv, null, null);
506                    // Trigger a refresh using the new, longer limit
507                    mailbox.mVisibleLimit += info.mVisibleLimitIncrement;
508                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
509                }
510            }
511        }.start();
512    }
513
514    /**
515     * @param messageId the id of message
516     * @return the accountId corresponding to the given messageId, or -1 if not found.
517     */
518    private long lookupAccountForMessage(long messageId) {
519        ContentResolver resolver = mProviderContext.getContentResolver();
520        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
521                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
522                                  new String[] { Long.toString(messageId) }, null);
523        try {
524            return c.moveToFirst()
525                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
526                : -1;
527        } finally {
528            c.close();
529        }
530    }
531
532    /**
533     * Delete a single message by moving it to the trash.
534     *
535     * This function has no callback, no result reporting, because the desired outcome
536     * is reflected entirely by changes to one or more cursors.
537     *
538     * @param messageId The id of the message to "delete".
539     * @param accountId The id of the message's account, or -1 if not known by caller
540     *
541     * TODO: Move out of UI thread
542     * TODO: "get account a for message m" should be a utility
543     * TODO: "get mailbox of type n for account a" should be a utility
544     */
545    public void deleteMessage(long messageId, long accountId) {
546        ContentResolver resolver = mProviderContext.getContentResolver();
547
548        // 1.  Look up acct# for message we're deleting
549        Cursor c = null;
550        if (accountId == -1) {
551            accountId = lookupAccountForMessage(messageId);
552        }
553        if (accountId == -1) {
554            return;
555        }
556
557        // 2. Confirm that there is a trash mailbox available
558        // 3.  If there's no trash mailbox, create one
559        // TODO: Does this need to be signaled explicitly to the sync engines?
560        long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
561
562        // 4.  Change the mailbox key for the message we're "deleting"
563        ContentValues cv = new ContentValues();
564        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
565        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
566        resolver.update(uri, cv, null, null);
567
568        // 5.  Drop non-essential data for the message (e.g. attachment files)
569        AttachmentProvider.deleteAllAttachmentFiles(mContext, accountId, messageId);
570
571        // 6.  Service runs automatically, MessagingController needs a kick
572        final Message message = Message.restoreMessageWithId(mContext, messageId);
573        Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
574        if (isMessagingController(account)) {
575            new Thread() {
576                @Override
577                public void run() {
578                    mLegacyController.processPendingCommands(message.mAccountKey);
579                }
580            }.start();
581        }
582    }
583
584    /**
585     * Set/clear the unread status of a message
586     *
587     * TODO db ops should not be in this thread. queue it up.
588     *
589     * @param messageId the message to update
590     * @param isRead the new value for the isRead flag
591     */
592    public void setMessageRead(final long messageId, boolean isRead) {
593        ContentValues cv = new ContentValues();
594        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
595        Uri uri = ContentUris.withAppendedId(
596                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
597        mProviderContext.getContentResolver().update(uri, cv, null, null);
598
599        // Service runs automatically, MessagingController needs a kick
600        final Message message = Message.restoreMessageWithId(mContext, messageId);
601        Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
602        if (isMessagingController(account)) {
603            new Thread() {
604                @Override
605                public void run() {
606                    mLegacyController.processPendingCommands(message.mAccountKey);
607                }
608            }.start();
609        }
610    }
611
612    /**
613     * Set/clear the favorite status of a message
614     *
615     * TODO db ops should not be in this thread. queue it up.
616     *
617     * @param messageId the message to update
618     * @param isFavorite the new value for the isFavorite flag
619     */
620    public void setMessageFavorite(final long messageId, boolean isFavorite) {
621        ContentValues cv = new ContentValues();
622        cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
623        Uri uri = ContentUris.withAppendedId(
624                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
625        mProviderContext.getContentResolver().update(uri, cv, null, null);
626
627        // Service runs automatically, MessagingController needs a kick
628        final Message message = Message.restoreMessageWithId(mContext, messageId);
629        Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
630        if (isMessagingController(account)) {
631            new Thread() {
632                @Override
633                public void run() {
634                    mLegacyController.processPendingCommands(message.mAccountKey);
635                }
636            }.start();
637        }
638    }
639
640    /**
641     * Request that an attachment be loaded.  It will be stored at a location controlled
642     * by the AttachmentProvider.
643     *
644     * @param attachmentId the attachment to load
645     * @param messageId the owner message
646     * @param mailboxId the owner mailbox
647     * @param accountId the owner account
648     * @param callback the Controller callback by which results will be reported
649     */
650    public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
651            final long accountId, final Result callback) {
652
653        File saveToFile = AttachmentProvider.getAttachmentFilename(mContext,
654                accountId, attachmentId);
655        if (saveToFile.exists()) {
656            // The attachment has already been downloaded, so we will just "pretend" to download it
657            synchronized (mListeners) {
658                for (Result listener : mListeners) {
659                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
660                }
661                for (Result listener : mListeners) {
662                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
663                }
664            }
665            return;
666        }
667
668        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
669
670        // Split here for target type (Service or MessagingController)
671        IEmailService service = getServiceForMessage(messageId);
672        if (service != null) {
673            // Service implementation
674            try {
675                service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
676                        AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
677            } catch (RemoteException e) {
678                // TODO Change exception handling to be consistent with however this method
679                // is implemented for other protocols
680                Log.e("onDownloadAttachment", "RemoteException", e);
681            }
682        } else {
683            // MessagingController implementation
684            new Thread() {
685                @Override
686                public void run() {
687                    mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
688                            mLegacyListener);
689                }
690            }.start();
691        }
692    }
693
694    /**
695     * For a given message id, return a service proxy if applicable, or null.
696     *
697     * @param messageId the message of interest
698     * @result service proxy, or null if n/a
699     */
700    private IEmailService getServiceForMessage(long messageId) {
701        // TODO make this more efficient, caching the account, smaller lookup here, etc.
702        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
703        return getServiceForAccount(message.mAccountKey);
704    }
705
706    /**
707     * For a given account id, return a service proxy if applicable, or null.
708     *
709     * TODO this should use a cache because we'll be doing this a lot
710     *
711     * @param accountId the message of interest
712     * @result service proxy, or null if n/a
713     */
714    private IEmailService getServiceForAccount(long accountId) {
715        // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc.
716        Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
717        if (isMessagingController(account)) {
718            return null;
719        } else {
720            return new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback);
721        }
722    }
723
724    /**
725     * Simple helper to determine if legacy MessagingController should be used
726     *
727     * TODO this should not require a full account, just an accountId
728     * TODO this should use a cache because we'll be doing this a lot
729     */
730    public boolean isMessagingController(EmailContent.Account account) {
731        Store.StoreInfo info =
732            Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext);
733        String scheme = info.mScheme;
734
735        return ("pop3".equals(scheme) || "imap".equals(scheme));
736    }
737
738    /**
739     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
740     * and the result is observed via provider cursors.  The callback will *not* necessarily be
741     * made from the UI thread, so you may need further handlers to safely make UI updates.
742     */
743    public interface Result {
744        /**
745         * Callback for updateMailboxList
746         *
747         * @param result If null, the operation completed without error
748         * @param accountId The account being operated on
749         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
750         */
751        public void updateMailboxListCallback(MessagingException result, long accountId,
752                int progress);
753
754        /**
755         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
756         * it's a separate call used only by UI's, so we can keep things separate.
757         *
758         * @param result If null, the operation completed without error
759         * @param accountId The account being operated on
760         * @param mailboxId The mailbox being operated on
761         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
762         * @param numNewMessages the number of new messages delivered
763         */
764        public void updateMailboxCallback(MessagingException result, long accountId,
765                long mailboxId, int progress, int numNewMessages);
766
767        /**
768         * Callback for loadMessageForView
769         *
770         * @param result if null, the attachment completed - if non-null, terminating with failure
771         * @param messageId the message which contains the attachment
772         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
773         */
774        public void loadMessageForViewCallback(MessagingException result, long messageId,
775                int progress);
776
777        /**
778         * Callback for loadAttachment
779         *
780         * @param result if null, the attachment completed - if non-null, terminating with failure
781         * @param messageId the message which contains the attachment
782         * @param attachmentId the attachment being loaded
783         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
784         */
785        public void loadAttachmentCallback(MessagingException result, long messageId,
786                long attachmentId, int progress);
787
788        /**
789         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
790         * it's a separate call used only by the automatic checker service, so we can keep
791         * things separate.
792         *
793         * @param result If null, the operation completed without error
794         * @param accountId The account being operated on
795         * @param mailboxId The mailbox being operated on (may be unknown at start)
796         * @param progress 0 for "starting", no updates, 100 for complete
797         * @param tag the same tag that was passed to serviceCheckMail()
798         */
799        public void serviceCheckMailCallback(MessagingException result, long accountId,
800                long mailboxId, int progress, long tag);
801
802        /**
803         * Callback for sending pending messages.  This will be called once to start the
804         * group, multiple times for messages, and once to complete the group.
805         *
806         * @param result If null, the operation completed without error
807         * @param accountId The account being operated on
808         * @param messageId The being sent (may be unknown at start)
809         * @param progress 0 for "starting", 100 for complete
810         */
811        public void sendMailCallback(MessagingException result, long accountId,
812                long messageId, int progress);
813    }
814
815    /**
816     * Support for receiving callbacks from MessagingController and dealing with UI going
817     * out of scope.
818     */
819    private class LegacyListener extends MessagingListener {
820
821        @Override
822        public void listFoldersStarted(EmailContent.Account account) {
823            synchronized (mListeners) {
824                for (Result l : mListeners) {
825                    l.updateMailboxListCallback(null, account.mId, 0);
826                }
827            }
828        }
829
830        @Override
831        public void listFoldersFailed(EmailContent.Account account, String message) {
832            synchronized (mListeners) {
833                for (Result l : mListeners) {
834                    l.updateMailboxListCallback(new MessagingException(message), account.mId, 0);
835                }
836            }
837        }
838
839        @Override
840        public void listFoldersFinished(EmailContent.Account account) {
841            synchronized (mListeners) {
842                for (Result l : mListeners) {
843                    l.updateMailboxListCallback(null, account.mId, 100);
844                }
845            }
846        }
847
848        @Override
849        public void synchronizeMailboxStarted(EmailContent.Account account,
850                EmailContent.Mailbox folder) {
851            synchronized (mListeners) {
852                for (Result l : mListeners) {
853                    l.updateMailboxCallback(null, account.mId, folder.mId, 0, 0);
854                }
855            }
856        }
857
858        @Override
859        public void synchronizeMailboxFinished(EmailContent.Account account,
860                EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) {
861            synchronized (mListeners) {
862                for (Result l : mListeners) {
863                    l.updateMailboxCallback(null, account.mId, folder.mId, 100, numNewMessages);
864                }
865            }
866        }
867
868        @Override
869        public void synchronizeMailboxFailed(EmailContent.Account account,
870                EmailContent.Mailbox folder, Exception e) {
871            MessagingException me;
872            if (e instanceof MessagingException) {
873                me = (MessagingException) e;
874            } else {
875                me = new MessagingException(e.toString());
876            }
877            synchronized (mListeners) {
878                for (Result l : mListeners) {
879                    l.updateMailboxCallback(me, account.mId, folder.mId, 0, 0);
880                }
881            }
882        }
883
884        @Override
885        public void checkMailStarted(Context context, long accountId, long tag) {
886            synchronized (mListeners) {
887                for (Result l : mListeners) {
888                    l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
889                }
890            }
891        }
892
893        @Override
894        public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
895            synchronized (mListeners) {
896                for (Result l : mListeners) {
897                    l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
898                }
899            }
900        }
901
902        @Override
903        public void loadMessageForViewStarted(long messageId) {
904            synchronized (mListeners) {
905                for (Result listener : mListeners) {
906                    listener.loadMessageForViewCallback(null, messageId, 0);
907                }
908            }
909        }
910
911        @Override
912        public void loadMessageForViewFinished(long messageId) {
913            synchronized (mListeners) {
914                for (Result listener : mListeners) {
915                    listener.loadMessageForViewCallback(null, messageId, 100);
916                }
917            }
918        }
919
920        @Override
921        public void loadMessageForViewFailed(long messageId, String message) {
922            synchronized (mListeners) {
923                for (Result listener : mListeners) {
924                    listener.loadMessageForViewCallback(new MessagingException(message),
925                            messageId, 0);
926                }
927            }
928        }
929
930        @Override
931        public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
932                boolean requiresDownload) {
933            synchronized (mListeners) {
934                for (Result listener : mListeners) {
935                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
936                }
937            }
938        }
939
940        @Override
941        public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
942            synchronized (mListeners) {
943                for (Result listener : mListeners) {
944                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
945                }
946            }
947        }
948
949        @Override
950        public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
951                String reason) {
952            synchronized (mListeners) {
953                for (Result listener : mListeners) {
954                    listener.loadAttachmentCallback(new MessagingException(reason),
955                            messageId, attachmentId, 0);
956                }
957            }
958        }
959
960        @Override
961        synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
962            synchronized (mListeners) {
963                for (Result listener : mListeners) {
964                    listener.sendMailCallback(null, accountId, messageId, 0);
965                }
966            }
967        }
968
969        @Override
970        synchronized public void sendPendingMessagesCompleted(long accountId) {
971            synchronized (mListeners) {
972                for (Result listener : mListeners) {
973                    listener.sendMailCallback(null, accountId, -1, 100);
974                }
975            }
976        }
977
978        @Override
979        synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
980                Exception reason) {
981            MessagingException me;
982            if (reason instanceof MessagingException) {
983                me = (MessagingException) reason;
984            } else {
985                me = new MessagingException(reason.toString());
986            }
987            synchronized (mListeners) {
988                for (Result listener : mListeners) {
989                    listener.sendMailCallback(me, accountId, messageId, 0);
990                }
991            }
992        }
993    }
994
995    /**
996     * Service callback for service operations
997     */
998    private class ServiceCallback extends IEmailServiceCallback.Stub {
999
1000        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1001
1002        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1003                int progress) {
1004            MessagingException result = null;
1005            switch (statusCode) {
1006                case EmailServiceStatus.SUCCESS:
1007                    progress = 100;
1008                    break;
1009                case EmailServiceStatus.IN_PROGRESS:
1010                    if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1011                        result = new MessagingException(
1012                                String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1013                    }
1014                    // discard progress reports that look like sentinels
1015                    if (progress < 0 || progress >= 100) {
1016                        return;
1017                    }
1018                    break;
1019                default:
1020                    result = new MessagingException(String.valueOf(statusCode));
1021                break;
1022            }
1023            synchronized (mListeners) {
1024                for (Result listener : mListeners) {
1025                    listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
1026                }
1027            }
1028        }
1029
1030        public void sendMessageStatus(long messageId, String subject, int statusCode,
1031                int progress) {
1032            // TODO Auto-generated method stub
1033
1034        }
1035
1036        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1037            MessagingException result= null;
1038            switch (statusCode) {
1039                case EmailServiceStatus.SUCCESS:
1040                    progress = 100;
1041                    break;
1042                case EmailServiceStatus.IN_PROGRESS:
1043                    // discard progress reports that look like sentinels
1044                    if (progress < 0 || progress >= 100) {
1045                        return;
1046                    }
1047                    break;
1048                default:
1049                    result = new MessagingException(String.valueOf(statusCode));
1050                break;
1051            }
1052            synchronized(mListeners) {
1053                for (Result listener : mListeners) {
1054                    listener.updateMailboxListCallback(result, accountId, progress);
1055                }
1056            }
1057        }
1058
1059        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1060            MessagingException result= null;
1061            switch (statusCode) {
1062                case EmailServiceStatus.SUCCESS:
1063                    progress = 100;
1064                    break;
1065                case EmailServiceStatus.IN_PROGRESS:
1066                    // discard progress reports that look like sentinels
1067                    if (progress < 0 || progress >= 100) {
1068                        return;
1069                    }
1070                    break;
1071                default:
1072                    result = new MessagingException(String.valueOf(statusCode));
1073                break;
1074            }
1075            // TODO where do we get "number of new messages" as well?
1076            // TODO should pass this back instead of looking it up here
1077            // TODO smaller projection
1078            Mailbox mbx = Mailbox.restoreMailboxWithId(mContext, mailboxId);
1079            // The mailbox could have disappeared if the server commanded it
1080            if (mbx == null) return;
1081            long accountId = mbx.mAccountKey;
1082            synchronized(mListeners) {
1083                for (Result listener : mListeners) {
1084                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
1085                }
1086            }
1087        }
1088    }
1089}
1090