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