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