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