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