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