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            if (account == null) {
409                return;
410            }
411            final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
412            new Thread() {
413                @Override
414                public void run() {
415                    mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
416                }
417            }.start();
418        }
419    }
420
421    /**
422     * Try to send all pending messages for a given account
423     *
424     * @param accountId the account for which to send messages (-1 for all accounts)
425     * @param callback
426     */
427    public void sendPendingMessages(long accountId, Result callback) {
428        // 1. make sure we even have an outbox, exit early if not
429        final long outboxId =
430            Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
431        if (outboxId == Mailbox.NO_MAILBOX) {
432            return;
433        }
434
435        // 2. dispatch as necessary
436        IEmailService service = getServiceForAccount(accountId);
437        if (service != null) {
438            // Service implementation
439            try {
440                service.startSync(outboxId);
441            } catch (RemoteException e) {
442                // TODO Change exception handling to be consistent with however this method
443                // is implemented for other protocols
444                Log.d("updateMailbox", "RemoteException" + e);
445            }
446        } else {
447            // MessagingController implementation
448            final EmailContent.Account account =
449                EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
450            if (account == null) {
451                return;
452            }
453            final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
454            new Thread() {
455                @Override
456                public void run() {
457                    mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
458                }
459            }.start();
460        }
461    }
462
463    /**
464     * Reset visible limits for all accounts.
465     * For each account:
466     *   look up limit
467     *   write limit into all mailboxes for that account
468     */
469    public void resetVisibleLimits() {
470        new Thread() {
471            @Override
472            public void run() {
473                ContentResolver resolver = mProviderContext.getContentResolver();
474                Cursor c = null;
475                try {
476                    c = resolver.query(
477                            Account.CONTENT_URI,
478                            Account.ID_PROJECTION,
479                            null, null, null);
480                    while (c.moveToNext()) {
481                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
482                        Account account = Account.restoreAccountWithId(mProviderContext, accountId);
483                        if (account != null) {
484                            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
485                                    account.getStoreUri(mProviderContext), mContext);
486                            if (info != null && info.mVisibleLimitDefault > 0) {
487                                int limit = info.mVisibleLimitDefault;
488                                ContentValues cv = new ContentValues();
489                                cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
490                                resolver.update(Mailbox.CONTENT_URI, cv,
491                                        MailboxColumns.ACCOUNT_KEY + "=?",
492                                        new String[] { Long.toString(accountId) });
493                            }
494                        }
495                    }
496                } finally {
497                    if (c != null) {
498                        c.close();
499                    }
500                }
501            }
502        }.start();
503    }
504
505    /**
506     * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
507     * IMAP and POP.
508     *
509     * @param mailboxId the mailbox
510     * @param callback
511     */
512    public void loadMoreMessages(final long mailboxId, Result callback) {
513        new Thread() {
514            @Override
515            public void run() {
516                Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
517                if (mailbox == null) {
518                    return;
519                }
520                Account account = Account.restoreAccountWithId(mProviderContext,
521                        mailbox.mAccountKey);
522                if (account == null) {
523                    return;
524                }
525                Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
526                        account.getStoreUri(mProviderContext), mContext);
527                if (info != null && info.mVisibleLimitIncrement > 0) {
528                    // Use provider math to increment the field
529                    ContentValues cv = new ContentValues();;
530                    cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
531                    cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement);
532                    Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
533                    mProviderContext.getContentResolver().update(uri, cv, null, null);
534                    // Trigger a refresh using the new, longer limit
535                    mailbox.mVisibleLimit += info.mVisibleLimitIncrement;
536                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
537                }
538            }
539        }.start();
540    }
541
542    /**
543     * @param messageId the id of message
544     * @return the accountId corresponding to the given messageId, or -1 if not found.
545     */
546    private long lookupAccountForMessage(long messageId) {
547        ContentResolver resolver = mProviderContext.getContentResolver();
548        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
549                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
550                                  new String[] { Long.toString(messageId) }, null);
551        try {
552            return c.moveToFirst()
553                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
554                : -1;
555        } finally {
556            c.close();
557        }
558    }
559
560    /**
561     * Delete a single attachment entry from the DB given its id.
562     * Does not delete any eventual associated files.
563     */
564    public void deleteAttachment(long attachmentId) {
565        ContentResolver resolver = mProviderContext.getContentResolver();
566        Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
567        resolver.delete(uri, null, null);
568    }
569
570    /**
571     * Delete a single message by moving it to the trash, or deleting it from the trash
572     *
573     * This function has no callback, no result reporting, because the desired outcome
574     * is reflected entirely by changes to one or more cursors.
575     *
576     * @param messageId The id of the message to "delete".
577     * @param accountId The id of the message's account, or -1 if not known by caller
578     *
579     * TODO: Move out of UI thread
580     * TODO: "get account a for message m" should be a utility
581     * TODO: "get mailbox of type n for account a" should be a utility
582     */
583    public void deleteMessage(long messageId, long accountId) {
584        ContentResolver resolver = mProviderContext.getContentResolver();
585
586        // 1.  Look up acct# for message we're deleting
587        if (accountId == -1) {
588            accountId = lookupAccountForMessage(messageId);
589        }
590        if (accountId == -1) {
591            return;
592        }
593
594        // 2. Confirm that there is a trash mailbox available.  If not, create one
595        long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH);
596
597        // 3.  Are we moving to trash or deleting?  It depends on where the message currently sits.
598        long sourceMailboxId = -1;
599        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
600                MESSAGEID_TO_MAILBOXID_PROJECTION, EmailContent.RECORD_ID + "=?",
601                new String[] { Long.toString(messageId) }, null);
602        try {
603            sourceMailboxId = c.moveToFirst()
604                ? c.getLong(MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID)
605                : -1;
606        } finally {
607            c.close();
608        }
609
610        // 4.  Drop non-essential data for the message (e.g. attachment files)
611        AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, accountId, messageId);
612
613        Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
614
615        // 5. Perform "delete" as appropriate
616        if (sourceMailboxId == trashMailboxId) {
617            // 5a. Delete from trash
618            resolver.delete(uri, null, null);
619        } else {
620            // 5b. Move to trash
621            ContentValues cv = new ContentValues();
622            cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
623            resolver.update(uri, cv, null, null);
624        }
625
626        // 6.  Service runs automatically, MessagingController needs a kick
627        Account account = Account.restoreAccountWithId(mProviderContext, accountId);
628        if (account == null) {
629            return; // isMessagingController returns false for null, but let's make it clear.
630        }
631        if (isMessagingController(account)) {
632            final long syncAccountId = accountId;
633            new Thread() {
634                @Override
635                public void run() {
636                    mLegacyController.processPendingActions(syncAccountId);
637                }
638            }.start();
639        }
640    }
641
642    /**
643     * Set/clear the unread status of a message
644     *
645     * TODO db ops should not be in this thread. queue it up.
646     *
647     * @param messageId the message to update
648     * @param isRead the new value for the isRead flag
649     */
650    public void setMessageRead(final long messageId, boolean isRead) {
651        ContentValues cv = new ContentValues();
652        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
653        Uri uri = ContentUris.withAppendedId(
654                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
655        mProviderContext.getContentResolver().update(uri, cv, null, null);
656
657        // Service runs automatically, MessagingController needs a kick
658        final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
659        if (message == null) {
660            return;
661        }
662        Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
663        if (account == null) {
664            return; // isMessagingController returns false for null, but let's make it clear.
665        }
666        if (isMessagingController(account)) {
667            new Thread() {
668                @Override
669                public void run() {
670                    mLegacyController.processPendingActions(message.mAccountKey);
671                }
672            }.start();
673        }
674    }
675
676    /**
677     * Set/clear the favorite status of a message
678     *
679     * TODO db ops should not be in this thread. queue it up.
680     *
681     * @param messageId the message to update
682     * @param isFavorite the new value for the isFavorite flag
683     */
684    public void setMessageFavorite(final long messageId, boolean isFavorite) {
685        ContentValues cv = new ContentValues();
686        cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
687        Uri uri = ContentUris.withAppendedId(
688                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
689        mProviderContext.getContentResolver().update(uri, cv, null, null);
690
691        // Service runs automatically, MessagingController needs a kick
692        final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
693        if (message == null) {
694            return;
695        }
696        Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
697        if (account == null) {
698            return; // isMessagingController returns false for null, but let's make it clear.
699        }
700        if (isMessagingController(account)) {
701            new Thread() {
702                @Override
703                public void run() {
704                    mLegacyController.processPendingActions(message.mAccountKey);
705                }
706            }.start();
707        }
708    }
709
710    /**
711     * Respond to a meeting invitation.
712     *
713     * @param messageId the id of the invitation being responded to
714     * @param response the code representing the response to the invitation
715     * @callback the Controller callback by which results will be reported (currently not defined)
716     */
717    public void sendMeetingResponse(final long messageId, final int response,
718            final Result callback) {
719         // Split here for target type (Service or MessagingController)
720        IEmailService service = getServiceForMessage(messageId);
721        if (service != null) {
722            // Service implementation
723            try {
724                service.sendMeetingResponse(messageId, response);
725            } catch (RemoteException e) {
726                // TODO Change exception handling to be consistent with however this method
727                // is implemented for other protocols
728                Log.e("onDownloadAttachment", "RemoteException", e);
729            }
730        }
731    }
732
733    /**
734     * Request that an attachment be loaded.  It will be stored at a location controlled
735     * by the AttachmentProvider.
736     *
737     * @param attachmentId the attachment to load
738     * @param messageId the owner message
739     * @param mailboxId the owner mailbox
740     * @param accountId the owner account
741     * @param callback the Controller callback by which results will be reported
742     */
743    public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
744            final long accountId, final Result callback) {
745
746        File saveToFile = AttachmentProvider.getAttachmentFilename(mProviderContext,
747                accountId, attachmentId);
748        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
749        if (attachInfo == null) {
750            return;
751        }
752
753        if (saveToFile.exists() && attachInfo.mContentUri != null) {
754            // The attachment has already been downloaded, so we will just "pretend" to download it
755            synchronized (mListeners) {
756                for (Result listener : mListeners) {
757                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
758                }
759                for (Result listener : mListeners) {
760                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
761                }
762            }
763            return;
764        }
765
766        // Split here for target type (Service or MessagingController)
767        IEmailService service = getServiceForMessage(messageId);
768        if (service != null) {
769            // Service implementation
770            try {
771                service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
772                        AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
773            } catch (RemoteException e) {
774                // TODO Change exception handling to be consistent with however this method
775                // is implemented for other protocols
776                Log.e("onDownloadAttachment", "RemoteException", e);
777            }
778        } else {
779            // MessagingController implementation
780            new Thread() {
781                @Override
782                public void run() {
783                    mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
784                            mLegacyListener);
785                }
786            }.start();
787        }
788    }
789
790    /**
791     * For a given message id, return a service proxy if applicable, or null.
792     *
793     * @param messageId the message of interest
794     * @result service proxy, or null if n/a
795     */
796    private IEmailService getServiceForMessage(long messageId) {
797        // TODO make this more efficient, caching the account, smaller lookup here, etc.
798        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
799        if (message == null) {
800            return null;
801        }
802        return getServiceForAccount(message.mAccountKey);
803    }
804
805    /**
806     * For a given account id, return a service proxy if applicable, or null.
807     *
808     * TODO this should use a cache because we'll be doing this a lot
809     *
810     * @param accountId the message of interest
811     * @result service proxy, or null if n/a
812     */
813    private IEmailService getServiceForAccount(long accountId) {
814        // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc.
815        Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
816        if (account == null || isMessagingController(account)) {
817            return null;
818        } else {
819            return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
820        }
821    }
822
823    /**
824     * Simple helper to determine if legacy MessagingController should be used
825     *
826     * TODO this should not require a full account, just an accountId
827     * TODO this should use a cache because we'll be doing this a lot
828     */
829    public boolean isMessagingController(EmailContent.Account account) {
830        if (account == null) return false;
831        Store.StoreInfo info =
832            Store.StoreInfo.getStoreInfo(account.getStoreUri(mProviderContext), mContext);
833        // This null happens in testing.
834        if (info == null) {
835            return false;
836        }
837        String scheme = info.mScheme;
838
839        return ("pop3".equals(scheme) || "imap".equals(scheme));
840    }
841
842    /**
843     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
844     * and the result is observed via provider cursors.  The callback will *not* necessarily be
845     * made from the UI thread, so you may need further handlers to safely make UI updates.
846     */
847    public interface Result {
848        /**
849         * Callback for updateMailboxList
850         *
851         * @param result If null, the operation completed without error
852         * @param accountId The account being operated on
853         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
854         */
855        public void updateMailboxListCallback(MessagingException result, long accountId,
856                int progress);
857
858        /**
859         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
860         * it's a separate call used only by UI's, so we can keep things separate.
861         *
862         * @param result If null, the operation completed without error
863         * @param accountId The account being operated on
864         * @param mailboxId The mailbox being operated on
865         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
866         * @param numNewMessages the number of new messages delivered
867         */
868        public void updateMailboxCallback(MessagingException result, long accountId,
869                long mailboxId, int progress, int numNewMessages);
870
871        /**
872         * Callback for loadMessageForView
873         *
874         * @param result if null, the attachment completed - if non-null, terminating with failure
875         * @param messageId the message which contains the attachment
876         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
877         */
878        public void loadMessageForViewCallback(MessagingException result, long messageId,
879                int progress);
880
881        /**
882         * Callback for loadAttachment
883         *
884         * @param result if null, the attachment completed - if non-null, terminating with failure
885         * @param messageId the message which contains the attachment
886         * @param attachmentId the attachment being loaded
887         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
888         */
889        public void loadAttachmentCallback(MessagingException result, long messageId,
890                long attachmentId, int progress);
891
892        /**
893         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
894         * it's a separate call used only by the automatic checker service, so we can keep
895         * things separate.
896         *
897         * @param result If null, the operation completed without error
898         * @param accountId The account being operated on
899         * @param mailboxId The mailbox being operated on (may be unknown at start)
900         * @param progress 0 for "starting", no updates, 100 for complete
901         * @param tag the same tag that was passed to serviceCheckMail()
902         */
903        public void serviceCheckMailCallback(MessagingException result, long accountId,
904                long mailboxId, int progress, long tag);
905
906        /**
907         * Callback for sending pending messages.  This will be called once to start the
908         * group, multiple times for messages, and once to complete the group.
909         *
910         * @param result If null, the operation completed without error
911         * @param accountId The account being operated on
912         * @param messageId The being sent (may be unknown at start)
913         * @param progress 0 for "starting", 100 for complete
914         */
915        public void sendMailCallback(MessagingException result, long accountId,
916                long messageId, int progress);
917    }
918
919    /**
920     * Support for receiving callbacks from MessagingController and dealing with UI going
921     * out of scope.
922     */
923    private class LegacyListener extends MessagingListener {
924
925        @Override
926        public void listFoldersStarted(long accountId) {
927            synchronized (mListeners) {
928                for (Result l : mListeners) {
929                    l.updateMailboxListCallback(null, accountId, 0);
930                }
931            }
932        }
933
934        @Override
935        public void listFoldersFailed(long accountId, String message) {
936            synchronized (mListeners) {
937                for (Result l : mListeners) {
938                    l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
939                }
940            }
941        }
942
943        @Override
944        public void listFoldersFinished(long accountId) {
945            synchronized (mListeners) {
946                for (Result l : mListeners) {
947                    l.updateMailboxListCallback(null, accountId, 100);
948                }
949            }
950        }
951
952        @Override
953        public void synchronizeMailboxStarted(long accountId, long mailboxId) {
954            synchronized (mListeners) {
955                for (Result l : mListeners) {
956                    l.updateMailboxCallback(null, accountId, mailboxId, 0, 0);
957                }
958            }
959        }
960
961        @Override
962        public void synchronizeMailboxFinished(long accountId, long mailboxId,
963                int totalMessagesInMailbox, int numNewMessages) {
964            synchronized (mListeners) {
965                for (Result l : mListeners) {
966                    l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages);
967                }
968            }
969        }
970
971        @Override
972        public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
973            MessagingException me;
974            if (e instanceof MessagingException) {
975                me = (MessagingException) e;
976            } else {
977                me = new MessagingException(e.toString());
978            }
979            synchronized (mListeners) {
980                for (Result l : mListeners) {
981                    l.updateMailboxCallback(me, accountId, mailboxId, 0, 0);
982                }
983            }
984        }
985
986        @Override
987        public void checkMailStarted(Context context, long accountId, long tag) {
988            synchronized (mListeners) {
989                for (Result l : mListeners) {
990                    l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
991                }
992            }
993        }
994
995        @Override
996        public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
997            synchronized (mListeners) {
998                for (Result l : mListeners) {
999                    l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
1000                }
1001            }
1002        }
1003
1004        @Override
1005        public void loadMessageForViewStarted(long messageId) {
1006            synchronized (mListeners) {
1007                for (Result listener : mListeners) {
1008                    listener.loadMessageForViewCallback(null, messageId, 0);
1009                }
1010            }
1011        }
1012
1013        @Override
1014        public void loadMessageForViewFinished(long messageId) {
1015            synchronized (mListeners) {
1016                for (Result listener : mListeners) {
1017                    listener.loadMessageForViewCallback(null, messageId, 100);
1018                }
1019            }
1020        }
1021
1022        @Override
1023        public void loadMessageForViewFailed(long messageId, String message) {
1024            synchronized (mListeners) {
1025                for (Result listener : mListeners) {
1026                    listener.loadMessageForViewCallback(new MessagingException(message),
1027                            messageId, 0);
1028                }
1029            }
1030        }
1031
1032        @Override
1033        public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
1034                boolean requiresDownload) {
1035            synchronized (mListeners) {
1036                for (Result listener : mListeners) {
1037                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
1038                }
1039            }
1040        }
1041
1042        @Override
1043        public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
1044            synchronized (mListeners) {
1045                for (Result listener : mListeners) {
1046                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
1047                }
1048            }
1049        }
1050
1051        @Override
1052        public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
1053                String reason) {
1054            synchronized (mListeners) {
1055                for (Result listener : mListeners) {
1056                    listener.loadAttachmentCallback(new MessagingException(reason),
1057                            messageId, attachmentId, 0);
1058                }
1059            }
1060        }
1061
1062        @Override
1063        synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
1064            synchronized (mListeners) {
1065                for (Result listener : mListeners) {
1066                    listener.sendMailCallback(null, accountId, messageId, 0);
1067                }
1068            }
1069        }
1070
1071        @Override
1072        synchronized public void sendPendingMessagesCompleted(long accountId) {
1073            synchronized (mListeners) {
1074                for (Result listener : mListeners) {
1075                    listener.sendMailCallback(null, accountId, -1, 100);
1076                }
1077            }
1078        }
1079
1080        @Override
1081        synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
1082                Exception reason) {
1083            MessagingException me;
1084            if (reason instanceof MessagingException) {
1085                me = (MessagingException) reason;
1086            } else {
1087                me = new MessagingException(reason.toString());
1088            }
1089            synchronized (mListeners) {
1090                for (Result listener : mListeners) {
1091                    listener.sendMailCallback(me, accountId, messageId, 0);
1092                }
1093            }
1094        }
1095    }
1096
1097    /**
1098     * Service callback for service operations
1099     */
1100    private class ServiceCallback extends IEmailServiceCallback.Stub {
1101
1102        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1103
1104        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1105                int progress) {
1106            MessagingException result = mapStatusToException(statusCode);
1107            switch (statusCode) {
1108                case EmailServiceStatus.SUCCESS:
1109                    progress = 100;
1110                    break;
1111                case EmailServiceStatus.IN_PROGRESS:
1112                    if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1113                        result = new MessagingException(
1114                                String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1115                    }
1116                    // discard progress reports that look like sentinels
1117                    if (progress < 0 || progress >= 100) {
1118                        return;
1119                    }
1120                    break;
1121            }
1122            synchronized (mListeners) {
1123                for (Result listener : mListeners) {
1124                    listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
1125                }
1126            }
1127        }
1128
1129        /**
1130         * Note, this is an incomplete implementation of this callback, because we are
1131         * not getting things back from Service in quite the same way as from MessagingController.
1132         * However, this is sufficient for basic "progress=100" notification that message send
1133         * has just completed.
1134         */
1135        public void sendMessageStatus(long messageId, String subject, int statusCode,
1136                int progress) {
1137//            Log.d(Email.LOG_TAG, "sendMessageStatus: messageId=" + messageId
1138//                    + " statusCode=" + statusCode + " progress=" + progress);
1139//            Log.d(Email.LOG_TAG, "sendMessageStatus: subject=" + subject);
1140            long accountId = -1;        // This should be in the callback
1141            MessagingException result = mapStatusToException(statusCode);
1142            switch (statusCode) {
1143                case EmailServiceStatus.SUCCESS:
1144                    progress = 100;
1145                    break;
1146                case EmailServiceStatus.IN_PROGRESS:
1147                    // discard progress reports that look like sentinels
1148                    if (progress < 0 || progress >= 100) {
1149                        return;
1150                    }
1151                    break;
1152            }
1153//            Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId
1154//                    + " progress=" + progress);
1155            synchronized(mListeners) {
1156                for (Result listener : mListeners) {
1157                    listener.sendMailCallback(result, accountId, messageId, progress);
1158                }
1159            }
1160        }
1161
1162        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1163            MessagingException result = mapStatusToException(statusCode);
1164            switch (statusCode) {
1165                case EmailServiceStatus.SUCCESS:
1166                    progress = 100;
1167                    break;
1168                case EmailServiceStatus.IN_PROGRESS:
1169                    // discard progress reports that look like sentinels
1170                    if (progress < 0 || progress >= 100) {
1171                        return;
1172                    }
1173                    break;
1174            }
1175            synchronized(mListeners) {
1176                for (Result listener : mListeners) {
1177                    listener.updateMailboxListCallback(result, accountId, progress);
1178                }
1179            }
1180        }
1181
1182        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1183            MessagingException result = mapStatusToException(statusCode);
1184            switch (statusCode) {
1185                case EmailServiceStatus.SUCCESS:
1186                    progress = 100;
1187                    break;
1188                case EmailServiceStatus.IN_PROGRESS:
1189                    // discard progress reports that look like sentinels
1190                    if (progress < 0 || progress >= 100) {
1191                        return;
1192                    }
1193                    break;
1194            }
1195            // TODO where do we get "number of new messages" as well?
1196            // TODO should pass this back instead of looking it up here
1197            // TODO smaller projection
1198            Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1199            // The mailbox could have disappeared if the server commanded it
1200            if (mbx == null) return;
1201            long accountId = mbx.mAccountKey;
1202            synchronized(mListeners) {
1203                for (Result listener : mListeners) {
1204                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
1205                }
1206            }
1207        }
1208
1209        private MessagingException mapStatusToException(int statusCode) {
1210            switch (statusCode) {
1211                case EmailServiceStatus.SUCCESS:
1212                case EmailServiceStatus.IN_PROGRESS:
1213                    return null;
1214
1215                case EmailServiceStatus.LOGIN_FAILED:
1216                    return new AuthenticationFailedException("");
1217
1218                case EmailServiceStatus.CONNECTION_ERROR:
1219                    return new MessagingException(MessagingException.IOERROR);
1220
1221                case EmailServiceStatus.SECURITY_FAILURE:
1222                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1223
1224                case EmailServiceStatus.MESSAGE_NOT_FOUND:
1225                case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1226                case EmailServiceStatus.FOLDER_NOT_DELETED:
1227                case EmailServiceStatus.FOLDER_NOT_RENAMED:
1228                case EmailServiceStatus.FOLDER_NOT_CREATED:
1229                case EmailServiceStatus.REMOTE_EXCEPTION:
1230                    // TODO: define exception code(s) & UI string(s) for server-side errors
1231                default:
1232                    return new MessagingException(String.valueOf(statusCode));
1233            }
1234        }
1235    }
1236}
1237