Controller.java revision 11aea1efe45b23c1217b31be029a527c9dc460b8
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.mail.Folder.MessageRetrievalListener;
23import com.android.email.mail.store.Pop3Store.Pop3Message;
24import com.android.email.provider.AttachmentProvider;
25import com.android.email.provider.EmailContent;
26import com.android.email.provider.EmailContent.Account;
27import com.android.email.provider.EmailContent.Attachment;
28import com.android.email.provider.EmailContent.Body;
29import com.android.email.provider.EmailContent.Mailbox;
30import com.android.email.provider.EmailContent.MailboxColumns;
31import com.android.email.provider.EmailContent.Message;
32import com.android.email.provider.EmailContent.MessageColumns;
33import com.android.email.service.EmailServiceStatus;
34import com.android.email.service.IEmailService;
35import com.android.email.service.IEmailServiceCallback;
36
37import android.app.Service;
38import android.content.ContentResolver;
39import android.content.ContentUris;
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.Intent;
43import android.database.Cursor;
44import android.net.Uri;
45import android.os.AsyncTask;
46import android.os.Bundle;
47import android.os.IBinder;
48import android.os.RemoteCallbackList;
49import android.os.RemoteException;
50import android.text.TextUtils;
51import android.util.Log;
52
53import java.io.FileNotFoundException;
54import java.io.IOException;
55import java.io.InputStream;
56import java.security.InvalidParameterException;
57import java.util.Collection;
58import java.util.HashSet;
59import java.util.concurrent.ConcurrentHashMap;
60
61/**
62 * New central controller/dispatcher for Email activities that may require remote operations.
63 * Handles disambiguating between legacy MessagingController operations and newer provider/sync
64 * based code.  We implement Service to allow loadAttachment calls to be sent in a consistent manner
65 * to IMAP, POP3, and EAS by AttachmentDownloadService
66 */
67public class Controller {
68    private static final String TAG = "Controller";
69    private static Controller sInstance;
70    private final Context mContext;
71    private Context mProviderContext;
72    private final MessagingController mLegacyController;
73    private final LegacyListener mLegacyListener = new LegacyListener();
74    private final ServiceCallback mServiceCallback = new ServiceCallback();
75    private final HashSet<Result> mListeners = new HashSet<Result>();
76    /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap =
77        new ConcurrentHashMap<Long, Boolean>();
78
79
80    // Note that 0 is a syntactically valid account key; however there can never be an account
81    // with id = 0, so attempts to restore the account will return null.  Null values are
82    // handled properly within the code, so this won't cause any issues.
83    private static final long ATTACHMENT_MAILBOX_ACCOUNT_KEY = 0;
84    /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__";
85    /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__";
86    private static final String WHERE_TYPE_ATTACHMENT =
87        MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT;
88    private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?";
89
90    private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
91        EmailContent.RECORD_ID,
92        EmailContent.MessageColumns.ACCOUNT_KEY
93    };
94    private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
95
96    private static final String[] BODY_SOURCE_KEY_PROJECTION =
97        new String[] {Body.SOURCE_MESSAGE_KEY};
98    private static final int BODY_SOURCE_KEY_COLUMN = 0;
99    private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
100
101    private static final String[] MESSAGEID_TO_MAILBOXID_PROJECTION = new String[] {
102        EmailContent.RECORD_ID,
103        EmailContent.MessageColumns.MAILBOX_KEY
104    };
105    private static final int MESSAGEID_TO_MAILBOXID_COLUMN_MAILBOXID = 1;
106
107    // Service callbacks as set up via setCallback
108    private static RemoteCallbackList<IEmailServiceCallback> sCallbackList =
109        new RemoteCallbackList<IEmailServiceCallback>();
110
111    protected Controller(Context _context) {
112        mContext = _context.getApplicationContext();
113        mProviderContext = _context;
114        mLegacyController = MessagingController.getInstance(mProviderContext, this);
115        mLegacyController.addListener(mLegacyListener);
116    }
117
118    /**
119     * Cleanup for test.  Mustn't be called for the regular {@link Controller}, as it's a
120     * singleton and lives till the process finishes.
121     *
122     * <p>However, this method MUST be called for mock instances.
123     */
124    public void cleanupForTest() {
125        mLegacyController.removeListener(mLegacyListener);
126    }
127
128    /**
129     * Gets or creates the singleton instance of Controller.
130     */
131    public synchronized static Controller getInstance(Context _context) {
132        if (sInstance == null) {
133            sInstance = new Controller(_context);
134        }
135        return sInstance;
136    }
137
138    /**
139     * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
140     *
141     * Tests that use this method MUST clean it up by calling this method again with null.
142     */
143    public synchronized static void injectMockControllerForTest(Controller mockController) {
144        sInstance = mockController;
145    }
146
147    /**
148     * For testing only:  Inject a different context for provider access.  This will be
149     * used internally for access the underlying provider (e.g. getContentResolver().query()).
150     * @param providerContext the provider context to be used by this instance
151     */
152    public void setProviderContext(Context providerContext) {
153        mProviderContext = providerContext;
154    }
155
156    /**
157     * Any UI code that wishes for callback results (on async ops) should register their callback
158     * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
159     * problems when the command completes and the activity has already paused or finished.
160     * @param listener The callback that may be used in action methods
161     */
162    public void addResultCallback(Result listener) {
163        synchronized (mListeners) {
164            mListeners.add(listener);
165        }
166    }
167
168    /**
169     * Any UI code that no longer wishes for callback results (on async ops) should unregister
170     * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
171     * to prevent problems when the command completes and the activity has already paused or
172     * finished.
173     * @param listener The callback that may no longer be used
174     */
175    public void removeResultCallback(Result listener) {
176        synchronized (mListeners) {
177            mListeners.remove(listener);
178        }
179    }
180
181    public Collection<Result> getResultCallbacksForTest() {
182        return mListeners;
183    }
184
185    /**
186     * Delete all Messages that live in the attachment mailbox
187     */
188    public void deleteAttachmentMessages() {
189        // Note: There should only be one attachment mailbox at present
190        ContentResolver resolver = mProviderContext.getContentResolver();
191        Cursor c = null;
192        try {
193            c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
194                    WHERE_TYPE_ATTACHMENT, null, null);
195            while (c.moveToNext()) {
196                long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
197                // Must delete attachments BEFORE messages
198                AttachmentProvider.deleteAllMailboxAttachmentFiles(mProviderContext, 0, mailboxId);
199                resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY,
200                        new String[] {Long.toString(mailboxId)});
201           }
202        } finally {
203            if (c != null) {
204                c.close();
205            }
206        }
207    }
208
209    /**
210     * Returns the attachment Mailbox (where we store eml attachment Emails), creating one
211     * if necessary
212     * @return the account's temporary Mailbox
213     */
214    public Mailbox getAttachmentMailbox() {
215        Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI,
216                Mailbox.CONTENT_PROJECTION, WHERE_TYPE_ATTACHMENT, null, null);
217        try {
218            if (c.moveToFirst()) {
219                return new Mailbox().restore(c);
220            }
221        } finally {
222            c.close();
223        }
224        Mailbox m = new Mailbox();
225        m.mAccountKey = ATTACHMENT_MAILBOX_ACCOUNT_KEY;
226        m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID;
227        m.mFlagVisible = false;
228        m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID;
229        m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
230        m.mType = Mailbox.TYPE_ATTACHMENT;
231        m.save(mProviderContext);
232        return m;
233    }
234
235    /**
236     * Create a Message from the Uri and store it in the attachment mailbox
237     * @param uri the uri containing message content
238     * @return the Message or null
239     */
240    public Message loadMessageFromUri(Uri uri) {
241        Mailbox mailbox = getAttachmentMailbox();
242        if (mailbox == null) return null;
243        try {
244            InputStream is = mProviderContext.getContentResolver().openInputStream(uri);
245            try {
246                // First, create a Pop3Message from the attachment and then parse it
247                Pop3Message pop3Message = new Pop3Message(
248                        ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null);
249                pop3Message.parse(is);
250                // Now, pull out the header fields
251                Message msg = new Message();
252                LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId);
253                // Commit the message to the local store
254                msg.save(mProviderContext);
255                // Setup the rest of the message and mark it completely loaded
256                mLegacyController.copyOneMessageToProvider(pop3Message, msg,
257                        Message.FLAG_LOADED_COMPLETE, mProviderContext);
258                // Restore the complete message and return it
259                return Message.restoreMessageWithId(mProviderContext, msg.mId);
260            } catch (MessagingException e) {
261            } catch (IOException e) {
262            }
263        } catch (FileNotFoundException e) {
264        }
265        return null;
266    }
267
268    /**
269     * Enable/disable logging for external sync services
270     *
271     * Generally this should be called by anybody who changes Email.DEBUG
272     */
273    public void serviceLogging(int debugEnabled) {
274        IEmailService service = ExchangeUtils.getExchangeService(mContext, mServiceCallback);
275        try {
276            service.setLogging(debugEnabled);
277        } catch (RemoteException e) {
278            // TODO Change exception handling to be consistent with however this method
279            // is implemented for other protocols
280            Log.d("updateMailboxList", "RemoteException" + e);
281        }
282    }
283
284    /**
285     * Request a remote update of mailboxes for an account.
286     */
287    public void updateMailboxList(final long accountId) {
288        Utility.runAsync(new Runnable() {
289            @Override
290            public void run() {
291                final IEmailService service = getServiceForAccount(accountId);
292                if (service != null) {
293                    // Service implementation
294                    try {
295                        service.updateFolderList(accountId);
296                    } catch (RemoteException e) {
297                        // TODO Change exception handling to be consistent with however this method
298                        // is implemented for other protocols
299                        Log.d("updateMailboxList", "RemoteException" + e);
300                    }
301                } else {
302                    // MessagingController implementation
303                    mLegacyController.listFolders(accountId, mLegacyListener);
304                }
305            }
306        });
307    }
308
309    /**
310     * Request a remote update of a mailbox.  For use by the timed service.
311     *
312     * Functionally this is quite similar to updateMailbox(), but it's a separate API and
313     * separate callback in order to keep UI callbacks from affecting the service loop.
314     */
315    public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) {
316        IEmailService service = getServiceForAccount(accountId);
317        if (service != null) {
318            // Service implementation
319//            try {
320                // TODO this isn't quite going to work, because we're going to get the
321                // generic (UI) callbacks and not the ones we need to restart the ol' service.
322                // service.startSync(mailboxId, tag);
323            mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag);
324//            } catch (RemoteException e) {
325                // TODO Change exception handling to be consistent with however this method
326                // is implemented for other protocols
327//                Log.d("updateMailbox", "RemoteException" + e);
328//            }
329        } else {
330            // MessagingController implementation
331            Utility.runAsync(new Runnable() {
332                public void run() {
333                    mLegacyController.checkMail(accountId, tag, mLegacyListener);
334                }
335            });
336        }
337    }
338
339    /**
340     * Request a remote update of a mailbox.
341     *
342     * The contract here should be to try and update the headers ASAP, in order to populate
343     * a simple message list.  We should also at this point queue up a background task of
344     * downloading some/all of the messages in this mailbox, but that should be interruptable.
345     */
346    public void updateMailbox(final long accountId, final long mailboxId) {
347
348        IEmailService service = getServiceForAccount(accountId);
349        if (service != null) {
350            // Service implementation
351            try {
352                service.startSync(mailboxId);
353            } catch (RemoteException e) {
354                // TODO Change exception handling to be consistent with however this method
355                // is implemented for other protocols
356                Log.d("updateMailbox", "RemoteException" + e);
357            }
358        } else {
359            // MessagingController implementation
360            Utility.runAsync(new Runnable() {
361                public void run() {
362                    // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
363                    Account account =
364                        EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
365                    Mailbox mailbox =
366                        EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
367                    if (account == null || mailbox == null) {
368                        return;
369                    }
370                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
371                }
372            });
373        }
374    }
375
376    /**
377     * Request that any final work necessary be done, to load a message.
378     *
379     * Note, this assumes that the caller has already checked message.mFlagLoaded and that
380     * additional work is needed.  There is no optimization here for a message which is already
381     * loaded.
382     *
383     * @param messageId the message to load
384     * @param callback the Controller callback by which results will be reported
385     */
386    public void loadMessageForView(final long messageId) {
387
388        // Split here for target type (Service or MessagingController)
389        IEmailService service = getServiceForMessage(messageId);
390        if (service != null) {
391            // There is no service implementation, so we'll just jam the value, log the error,
392            // and get out of here.
393            Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
394            ContentValues cv = new ContentValues();
395            cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
396            mProviderContext.getContentResolver().update(uri, cv, null, null);
397            Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
398            synchronized (mListeners) {
399                for (Result listener : mListeners) {
400                    listener.loadMessageForViewCallback(null, messageId, 100);
401                }
402            }
403        } else {
404            // MessagingController implementation
405            Utility.runAsync(new Runnable() {
406                public void run() {
407                    mLegacyController.loadMessageForView(messageId, mLegacyListener);
408                }
409            });
410        }
411    }
412
413
414    /**
415     * Saves the message to a mailbox of given type.
416     * This is a synchronous operation taking place in the same thread as the caller.
417     * Upon return the message.mId is set.
418     * @param message the message (must have the mAccountId set).
419     * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
420     */
421    public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
422        long accountId = message.mAccountKey;
423        long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
424        message.mMailboxKey = mailboxId;
425        message.save(mProviderContext);
426    }
427
428    /**
429     * @param accountId the account id
430     * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
431     * @return the id of the mailbox. The mailbox is created if not existing.
432     * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
433     * Does not validate the input in other ways (e.g. does not verify the existence of account).
434     */
435    public long findOrCreateMailboxOfType(long accountId, int mailboxType) {
436        if (accountId < 0 || mailboxType < 0) {
437            return Mailbox.NO_MAILBOX;
438        }
439        long mailboxId =
440            Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
441        return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
442    }
443
444    /**
445     * Returns the server-side name for a specific mailbox.
446     *
447     * @param mailboxType the mailbox type
448     * @return the resource string corresponding to the mailbox type, empty if not found.
449     */
450    /* package */ String getMailboxServerName(int mailboxType) {
451        int resId = -1;
452        switch (mailboxType) {
453            case Mailbox.TYPE_INBOX:
454                resId = R.string.mailbox_name_server_inbox;
455                break;
456            case Mailbox.TYPE_OUTBOX:
457                resId = R.string.mailbox_name_server_outbox;
458                break;
459            case Mailbox.TYPE_DRAFTS:
460                resId = R.string.mailbox_name_server_drafts;
461                break;
462            case Mailbox.TYPE_TRASH:
463                resId = R.string.mailbox_name_server_trash;
464                break;
465            case Mailbox.TYPE_SENT:
466                resId = R.string.mailbox_name_server_sent;
467                break;
468            case Mailbox.TYPE_JUNK:
469                resId = R.string.mailbox_name_server_junk;
470                break;
471        }
472        return resId != -1 ? mContext.getString(resId) : "";
473    }
474
475    /**
476     * Create a mailbox given the account and mailboxType.
477     * TODO: Does this need to be signaled explicitly to the sync engines?
478     */
479    /* package */ long createMailbox(long accountId, int mailboxType) {
480        if (accountId < 0 || mailboxType < 0) {
481            String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
482            Log.e(Email.LOG_TAG, mes);
483            throw new RuntimeException(mes);
484        }
485        Mailbox box = new Mailbox();
486        box.mAccountKey = accountId;
487        box.mType = mailboxType;
488        box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
489        box.mFlagVisible = true;
490        box.mDisplayName = getMailboxServerName(mailboxType);
491        box.save(mProviderContext);
492        return box.mId;
493    }
494
495    /**
496     * Send a message:
497     * - move the message to Outbox (the message is assumed to be in Drafts).
498     * - EAS service will take it from there
499     * - trigger send for POP/IMAP
500     * @param messageId the id of the message to send
501     */
502    public void sendMessage(long messageId, long accountId) {
503        ContentResolver resolver = mProviderContext.getContentResolver();
504        if (accountId == -1) {
505            accountId = lookupAccountForMessage(messageId);
506        }
507        if (accountId == -1) {
508            // probably the message was not found
509            if (Email.LOGD) {
510                Email.log("no account found for message " + messageId);
511            }
512            return;
513        }
514
515        // Move to Outbox
516        long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
517        ContentValues cv = new ContentValues();
518        cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
519
520        // does this need to be SYNCED_CONTENT_URI instead?
521        Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
522        resolver.update(uri, cv, null, null);
523
524        sendPendingMessages(accountId);
525    }
526
527    private void sendPendingMessagesSmtp(long accountId) {
528        // for IMAP & POP only, (attempt to) send the message now
529        final EmailContent.Account account =
530                EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
531        if (account == null) {
532            return;
533        }
534        final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
535        Utility.runAsync(new Runnable() {
536            public void run() {
537                mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
538            }
539        });
540    }
541
542    /**
543     * Try to send all pending messages for a given account
544     *
545     * @param accountId the account for which to send messages
546     */
547    public void sendPendingMessages(long accountId) {
548        // 1. make sure we even have an outbox, exit early if not
549        final long outboxId =
550            Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
551        if (outboxId == Mailbox.NO_MAILBOX) {
552            return;
553        }
554
555        // 2. dispatch as necessary
556        IEmailService service = getServiceForAccount(accountId);
557        if (service != null) {
558            // Service implementation
559            try {
560                service.startSync(outboxId);
561            } catch (RemoteException e) {
562                // TODO Change exception handling to be consistent with however this method
563                // is implemented for other protocols
564                Log.d("updateMailbox", "RemoteException" + e);
565            }
566        } else {
567            // MessagingController implementation
568            sendPendingMessagesSmtp(accountId);
569        }
570    }
571
572    /**
573     * Reset visible limits for all accounts.
574     * For each account:
575     *   look up limit
576     *   write limit into all mailboxes for that account
577     */
578    public void resetVisibleLimits() {
579        Utility.runAsync(new Runnable() {
580            public void run() {
581                ContentResolver resolver = mProviderContext.getContentResolver();
582                Cursor c = null;
583                try {
584                    c = resolver.query(
585                            Account.CONTENT_URI,
586                            Account.ID_PROJECTION,
587                            null, null, null);
588                    while (c.moveToNext()) {
589                        long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
590                        Account account = Account.restoreAccountWithId(mProviderContext, accountId);
591                        if (account != null) {
592                            Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
593                                    account.getStoreUri(mProviderContext), mContext);
594                            if (info != null && info.mVisibleLimitDefault > 0) {
595                                int limit = info.mVisibleLimitDefault;
596                                ContentValues cv = new ContentValues();
597                                cv.put(MailboxColumns.VISIBLE_LIMIT, limit);
598                                resolver.update(Mailbox.CONTENT_URI, cv,
599                                        MailboxColumns.ACCOUNT_KEY + "=?",
600                                        new String[] { Long.toString(accountId) });
601                            }
602                        }
603                    }
604                } finally {
605                    if (c != null) {
606                        c.close();
607                    }
608                }
609            }
610        });
611    }
612
613    /**
614     * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
615     * IMAP and POP.
616     *
617     * @param mailboxId the mailbox
618     * @param callback
619     */
620    public void loadMoreMessages(final long mailboxId) {
621        Utility.runAsync(new Runnable() {
622            public void run() {
623                Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
624                if (mailbox == null) {
625                    return;
626                }
627                Account account = Account.restoreAccountWithId(mProviderContext,
628                        mailbox.mAccountKey);
629                if (account == null) {
630                    return;
631                }
632                Store.StoreInfo info = Store.StoreInfo.getStoreInfo(
633                        account.getStoreUri(mProviderContext), mContext);
634                if (info != null && info.mVisibleLimitIncrement > 0) {
635                    // Use provider math to increment the field
636                    ContentValues cv = new ContentValues();;
637                    cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
638                    cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement);
639                    Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
640                    mProviderContext.getContentResolver().update(uri, cv, null, null);
641                    // Trigger a refresh using the new, longer limit
642                    mailbox.mVisibleLimit += info.mVisibleLimitIncrement;
643                    mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
644                }
645            }
646        });
647    }
648
649    /**
650     * @param messageId the id of message
651     * @return the accountId corresponding to the given messageId, or -1 if not found.
652     */
653    private long lookupAccountForMessage(long messageId) {
654        ContentResolver resolver = mProviderContext.getContentResolver();
655        Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
656                                  MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
657                                  new String[] { Long.toString(messageId) }, null);
658        try {
659            return c.moveToFirst()
660                ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
661                : -1;
662        } finally {
663            c.close();
664        }
665    }
666
667    /**
668     * Delete a single attachment entry from the DB given its id.
669     * Does not delete any eventual associated files.
670     */
671    public void deleteAttachment(long attachmentId) {
672        ContentResolver resolver = mProviderContext.getContentResolver();
673        Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
674        resolver.delete(uri, null, null);
675    }
676
677    /**
678     * Delete a single message by moving it to the trash, or deleting it from the trash
679     *
680     * This function has no callback, no result reporting, because the desired outcome
681     * is reflected entirely by changes to one or more cursors.
682     *
683     * @param messageId The id of the message to "delete".
684     * @param accountId The id of the message's account, or -1 if not known by caller
685     * @return the AsyncTask used to execute the deletion
686     */
687    public AsyncTask<Void, Void, Void> deleteMessage(final long messageId, long accountId) {
688        return Utility.runAsync(new Runnable() {
689            public void run() {
690                // 1. Get the message's account
691                Account account = Account.getAccountForMessageId(mProviderContext, messageId);
692
693                // 2. Confirm that there is a trash mailbox available.  If not, create one
694                long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH);
695
696                // 3. Get the message's original mailbox
697                Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId);
698
699                // 4.  Drop non-essential data for the message (e.g. attachment files)
700                AttachmentProvider.deleteAllAttachmentFiles(mProviderContext, account.mId,
701                        messageId);
702
703                Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI,
704                        messageId);
705                ContentResolver resolver = mProviderContext.getContentResolver();
706
707                // 5. Perform "delete" as appropriate
708                if (mailbox.mId == trashMailboxId) {
709                    // 5a. Delete from trash
710                    resolver.delete(uri, null, null);
711                } else {
712                    // 5b. Move to trash
713                    ContentValues cv = new ContentValues();
714                    cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
715                    resolver.update(uri, cv, null, null);
716                }
717
718                if (isMessagingController(account)) {
719                    mLegacyController.processPendingActions(account.mId);
720                }
721            }
722        });
723    }
724
725    /**
726     * Moving messages to another folder
727     *
728     * This function has no callback, no result reporting, because the desired outcome
729     * is reflected entirely by changes to one or more cursors.
730     *
731     * Note this method assumes all the messages, and the destination mailbox belong to the same
732     * account.
733     *
734     * @param messageIds The IDs of the messages to move
735     * @param newMailboxId The id of the folder we're supposed to move the folder to
736     * @return the AsyncTask that will execute the move
737     */
738    public AsyncTask<Void, Void, Void> moveMessage(final long[] messageIds,
739            final long newMailboxId) {
740        if (messageIds == null || messageIds.length == 0) {
741            throw new InvalidParameterException();
742        }
743        return Utility.runAsync(new Runnable() {
744            public void run() {
745                Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]);
746                if (account != null) {
747                    ContentValues cv = new ContentValues();
748                    cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId);
749                    ContentResolver resolver = mProviderContext.getContentResolver();
750                    for (long messageId : messageIds) {
751                        Uri uri = ContentUris.withAppendedId(
752                                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
753                        resolver.update(uri, cv, null, null);
754                    }
755                    if (isMessagingController(account)) {
756                        mLegacyController.processPendingActions(account.mId);
757                    }
758                }
759            }
760        });
761    }
762
763    /**
764     * Set/clear the unread status of a message
765     *
766     * TODO db ops should not be in this thread. queue it up.
767     *
768     * @param messageId the message to update
769     * @param isRead the new value for the isRead flag
770     */
771    public void setMessageRead(final long messageId, boolean isRead) {
772        ContentValues cv = new ContentValues();
773        cv.put(EmailContent.MessageColumns.FLAG_READ, isRead);
774        Uri uri = ContentUris.withAppendedId(
775                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
776        mProviderContext.getContentResolver().update(uri, cv, null, null);
777
778        // Service runs automatically, MessagingController needs a kick
779        final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
780        if (message == null) {
781            return;
782        }
783        Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
784        if (account == null) {
785            return; // isMessagingController returns false for null, but let's make it clear.
786        }
787        if (isMessagingController(account)) {
788            Utility.runAsync(new Runnable() {
789                public void run() {
790                    mLegacyController.processPendingActions(message.mAccountKey);
791                }
792            });
793        }
794    }
795
796    /**
797     * Set/clear the favorite status of a message
798     *
799     * TODO db ops should not be in this thread. queue it up.
800     *
801     * @param messageId the message to update
802     * @param isFavorite the new value for the isFavorite flag
803     */
804    public void setMessageFavorite(final long messageId, boolean isFavorite) {
805        ContentValues cv = new ContentValues();
806        cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
807        Uri uri = ContentUris.withAppendedId(
808                EmailContent.Message.SYNCED_CONTENT_URI, messageId);
809        mProviderContext.getContentResolver().update(uri, cv, null, null);
810
811        // Service runs automatically, MessagingController needs a kick
812        final Message message = Message.restoreMessageWithId(mProviderContext, messageId);
813        if (message == null) {
814            return;
815        }
816        Account account = Account.restoreAccountWithId(mProviderContext, message.mAccountKey);
817        if (account == null) {
818            return; // isMessagingController returns false for null, but let's make it clear.
819        }
820        if (isMessagingController(account)) {
821            Utility.runAsync(new Runnable() {
822                public void run() {
823                    mLegacyController.processPendingActions(message.mAccountKey);
824                }
825            });
826        }
827    }
828
829    /**
830     * Respond to a meeting invitation.
831     *
832     * @param messageId the id of the invitation being responded to
833     * @param response the code representing the response to the invitation
834     */
835    public void sendMeetingResponse(final long messageId, final int response) {
836         // Split here for target type (Service or MessagingController)
837        IEmailService service = getServiceForMessage(messageId);
838        if (service != null) {
839            // Service implementation
840            try {
841                service.sendMeetingResponse(messageId, response);
842            } catch (RemoteException e) {
843                // TODO Change exception handling to be consistent with however this method
844                // is implemented for other protocols
845                Log.e("onDownloadAttachment", "RemoteException", e);
846            }
847        }
848    }
849
850    /**
851     * Request that an attachment be loaded.  It will be stored at a location controlled
852     * by the AttachmentProvider.
853     *
854     * @param attachmentId the attachment to load
855     * @param messageId the owner message
856     * @param mailboxId the owner mailbox
857     * @param accountId the owner account
858     */
859    public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId,
860            final long accountId) {
861
862        Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
863        if (Utility.attachmentExists(mProviderContext, attachInfo)) {
864            // The attachment has already been downloaded, so we will just "pretend" to download it
865            // This presumably is for POP3 messages
866            synchronized (mListeners) {
867                for (Result listener : mListeners) {
868                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
869                }
870                for (Result listener : mListeners) {
871                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
872                }
873            }
874            return;
875        }
876
877        // Flag the attachment as needing download at the user's request
878        ContentValues cv = new ContentValues();
879        cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
880        attachInfo.update(mContext, cv);
881    }
882
883    /**
884     * For a given message id, return a service proxy if applicable, or null.
885     *
886     * @param messageId the message of interest
887     * @result service proxy, or null if n/a
888     */
889    private IEmailService getServiceForMessage(long messageId) {
890        // TODO make this more efficient, caching the account, smaller lookup here, etc.
891        Message message = Message.restoreMessageWithId(mProviderContext, messageId);
892        if (message == null) {
893            return null;
894        }
895        return getServiceForAccount(message.mAccountKey);
896    }
897
898    /**
899     * For a given account id, return a service proxy if applicable, or null.
900     *
901     * @param accountId the message of interest
902     * @result service proxy, or null if n/a
903     */
904    private IEmailService getServiceForAccount(long accountId) {
905        if (isMessagingController(accountId)) return null;
906        return getExchangeEmailService();
907    }
908
909    private IEmailService getExchangeEmailService() {
910        return ExchangeUtils.getExchangeService(mContext, mServiceCallback);
911    }
912
913    /**
914     * Simple helper to determine if legacy MessagingController should be used
915     */
916    public boolean isMessagingController(EmailContent.Account account) {
917        if (account == null) return false;
918        return isMessagingController(account.mId);
919    }
920
921    public boolean isMessagingController(long accountId) {
922        Boolean isLegacyController = mLegacyControllerMap.get(accountId);
923        if (isLegacyController == null) {
924            String protocol = Account.getProtocol(mProviderContext, accountId);
925            isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol));
926            mLegacyControllerMap.put(accountId, isLegacyController);
927        }
928        return isLegacyController;
929    }
930
931    /**
932     * Delete an account.
933     */
934    public void deleteAccount(final long accountId) {
935        Utility.runAsync(new Runnable() {
936            public void run() {
937                deleteAccountSync(accountId);
938            }
939        });
940    }
941
942    /**
943     * Delete an account synchronously.  Intended to be used only by unit tests.
944     */
945    public void deleteAccountSync(long accountId) {
946        try {
947            mLegacyControllerMap.remove(accountId);
948            // Get the account URI.
949            final Account account = Account.restoreAccountWithId(mContext, accountId);
950            if (account == null) {
951                return; // Already deleted?
952            }
953
954            final String accountUri = account.getStoreUri(mContext);
955            // Delete Remote store at first.
956            if (!TextUtils.isEmpty(accountUri)) {
957                Store.getInstance(accountUri, mContext, null).delete();
958                // Remove the Store instance from cache.
959                Store.removeInstance(accountUri);
960            }
961
962            Uri uri = ContentUris.withAppendedId(
963                    EmailContent.Account.CONTENT_URI, accountId);
964            mContext.getContentResolver().delete(uri, null, null);
965
966            // Update the backup (side copy) of the accounts
967            AccountBackupRestore.backupAccounts(mContext);
968
969            // Release or relax device administration, if relevant
970            SecurityPolicy.getInstance(mContext).reducePolicies();
971
972            Email.setServicesEnabled(mContext);
973        } catch (Exception e) {
974            Log.w(Email.LOG_TAG, "Exception while deleting account", e);
975        } finally {
976            synchronized (mListeners) {
977                for (Result l : mListeners) {
978                    l.deleteAccountCallback(accountId);
979                }
980            }
981        }
982    }
983
984    /**
985     * Simple callback for synchronous commands.  For many commands, this can be largely ignored
986     * and the result is observed via provider cursors.  The callback will *not* necessarily be
987     * made from the UI thread, so you may need further handlers to safely make UI updates.
988     */
989    public static abstract class Result {
990        /**
991         * Callback for updateMailboxList
992         *
993         * @param result If null, the operation completed without error
994         * @param accountId The account being operated on
995         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
996         */
997        public void updateMailboxListCallback(MessagingException result, long accountId,
998                int progress) {
999        }
1000
1001        /**
1002         * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
1003         * it's a separate call used only by UI's, so we can keep things separate.
1004         *
1005         * @param result If null, the operation completed without error
1006         * @param accountId The account being operated on
1007         * @param mailboxId The mailbox being operated on
1008         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1009         * @param numNewMessages the number of new messages delivered
1010         */
1011        public void updateMailboxCallback(MessagingException result, long accountId,
1012                long mailboxId, int progress, int numNewMessages) {
1013        }
1014
1015        /**
1016         * Callback for loadMessageForView
1017         *
1018         * @param result if null, the attachment completed - if non-null, terminating with failure
1019         * @param messageId the message which contains the attachment
1020         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1021         */
1022        public void loadMessageForViewCallback(MessagingException result, long messageId,
1023                int progress) {
1024        }
1025
1026        /**
1027         * Callback for loadAttachment
1028         *
1029         * @param result if null, the attachment completed - if non-null, terminating with failure
1030         * @param messageId the message which contains the attachment
1031         * @param attachmentId the attachment being loaded
1032         * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
1033         */
1034        public void loadAttachmentCallback(MessagingException result, long messageId,
1035                long attachmentId, int progress) {
1036        }
1037
1038        /**
1039         * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
1040         * it's a separate call used only by the automatic checker service, so we can keep
1041         * things separate.
1042         *
1043         * @param result If null, the operation completed without error
1044         * @param accountId The account being operated on
1045         * @param mailboxId The mailbox being operated on (may be unknown at start)
1046         * @param progress 0 for "starting", no updates, 100 for complete
1047         * @param tag the same tag that was passed to serviceCheckMail()
1048         */
1049        public void serviceCheckMailCallback(MessagingException result, long accountId,
1050                long mailboxId, int progress, long tag) {
1051        }
1052
1053        /**
1054         * Callback for sending pending messages.  This will be called once to start the
1055         * group, multiple times for messages, and once to complete the group.
1056         *
1057         * @param result If null, the operation completed without error
1058         * @param accountId The account being operated on
1059         * @param messageId The being sent (may be unknown at start)
1060         * @param progress 0 for "starting", 100 for complete
1061         */
1062        public void sendMailCallback(MessagingException result, long accountId,
1063                long messageId, int progress) {
1064        }
1065
1066        /**
1067         * Callback from {@link Controller#deleteAccount}.
1068         */
1069        public void deleteAccountCallback(long accountId) {
1070        }
1071    }
1072
1073    /**
1074     * Support for receiving callbacks from MessagingController and dealing with UI going
1075     * out of scope.
1076     */
1077    public class LegacyListener extends MessagingListener implements MessageRetrievalListener {
1078        public LegacyListener(long messageId, long attachmentId) {
1079            super(messageId, attachmentId);
1080        }
1081
1082        public LegacyListener() {
1083        }
1084
1085        @Override
1086        public void listFoldersStarted(long accountId) {
1087            synchronized (mListeners) {
1088                for (Result l : mListeners) {
1089                    l.updateMailboxListCallback(null, accountId, 0);
1090                }
1091            }
1092        }
1093
1094        @Override
1095        public void listFoldersFailed(long accountId, String message) {
1096            synchronized (mListeners) {
1097                for (Result l : mListeners) {
1098                    l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
1099                }
1100            }
1101        }
1102
1103        @Override
1104        public void listFoldersFinished(long accountId) {
1105            synchronized (mListeners) {
1106                for (Result l : mListeners) {
1107                    l.updateMailboxListCallback(null, accountId, 100);
1108                }
1109            }
1110        }
1111
1112        @Override
1113        public void synchronizeMailboxStarted(long accountId, long mailboxId) {
1114            synchronized (mListeners) {
1115                for (Result l : mListeners) {
1116                    l.updateMailboxCallback(null, accountId, mailboxId, 0, 0);
1117                }
1118            }
1119        }
1120
1121        @Override
1122        public void synchronizeMailboxFinished(long accountId, long mailboxId,
1123                int totalMessagesInMailbox, int numNewMessages) {
1124            synchronized (mListeners) {
1125                for (Result l : mListeners) {
1126                    l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages);
1127                }
1128            }
1129        }
1130
1131        @Override
1132        public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
1133            MessagingException me;
1134            if (e instanceof MessagingException) {
1135                me = (MessagingException) e;
1136            } else {
1137                me = new MessagingException(e.toString());
1138            }
1139            synchronized (mListeners) {
1140                for (Result l : mListeners) {
1141                    l.updateMailboxCallback(me, accountId, mailboxId, 0, 0);
1142                }
1143            }
1144        }
1145
1146        @Override
1147        public void checkMailStarted(Context context, long accountId, long tag) {
1148            synchronized (mListeners) {
1149                for (Result l : mListeners) {
1150                    l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
1151                }
1152            }
1153        }
1154
1155        @Override
1156        public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
1157            synchronized (mListeners) {
1158                for (Result l : mListeners) {
1159                    l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
1160                }
1161            }
1162        }
1163
1164        @Override
1165        public void loadMessageForViewStarted(long messageId) {
1166            synchronized (mListeners) {
1167                for (Result listener : mListeners) {
1168                    listener.loadMessageForViewCallback(null, messageId, 0);
1169                }
1170            }
1171        }
1172
1173        @Override
1174        public void loadMessageForViewFinished(long messageId) {
1175            synchronized (mListeners) {
1176                for (Result listener : mListeners) {
1177                    listener.loadMessageForViewCallback(null, messageId, 100);
1178                }
1179            }
1180        }
1181
1182        @Override
1183        public void loadMessageForViewFailed(long messageId, String message) {
1184            synchronized (mListeners) {
1185                for (Result listener : mListeners) {
1186                    listener.loadMessageForViewCallback(new MessagingException(message),
1187                            messageId, 0);
1188                }
1189            }
1190        }
1191
1192        @Override
1193        public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
1194                boolean requiresDownload) {
1195            try {
1196                mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
1197                        EmailServiceStatus.IN_PROGRESS, 0);
1198            } catch (RemoteException e) {
1199            }
1200            synchronized (mListeners) {
1201                for (Result listener : mListeners) {
1202                    listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
1203                }
1204            }
1205        }
1206
1207        @Override
1208        public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
1209            try {
1210                mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
1211                        EmailServiceStatus.SUCCESS, 100);
1212            } catch (RemoteException e) {
1213            }
1214            synchronized (mListeners) {
1215                for (Result listener : mListeners) {
1216                    listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
1217                }
1218            }
1219        }
1220
1221        @Override
1222        public void loadAttachmentProgress(int progress) {
1223            synchronized (mListeners) {
1224                for (Result listener : mListeners) {
1225                    listener.loadAttachmentCallback(null, messageId, attachmentId, progress);
1226               }
1227            }
1228        }
1229
1230        @Override
1231        public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
1232                MessagingException me) {
1233            try {
1234                // If the cause of the MessagingException is an IOException, we send a status of
1235                // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to
1236                // download the attachment.  Otherwise, the error is considered non-recoverable.
1237                int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND;
1238                if (me.getCause() instanceof IOException) {
1239                    status = EmailServiceStatus.CONNECTION_ERROR;
1240                }
1241                mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0);
1242            } catch (RemoteException e) {
1243            }
1244            synchronized (mListeners) {
1245                for (Result listener : mListeners) {
1246                    listener.loadAttachmentCallback(me, messageId, attachmentId, 0);
1247                }
1248            }
1249        }
1250
1251        @Override
1252        synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
1253            synchronized (mListeners) {
1254                for (Result listener : mListeners) {
1255                    listener.sendMailCallback(null, accountId, messageId, 0);
1256                }
1257            }
1258        }
1259
1260        @Override
1261        synchronized public void sendPendingMessagesCompleted(long accountId) {
1262            synchronized (mListeners) {
1263                for (Result listener : mListeners) {
1264                    listener.sendMailCallback(null, accountId, -1, 100);
1265                }
1266            }
1267        }
1268
1269        @Override
1270        synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
1271                Exception reason) {
1272            MessagingException me;
1273            if (reason instanceof MessagingException) {
1274                me = (MessagingException) reason;
1275            } else {
1276                me = new MessagingException(reason.toString());
1277            }
1278            synchronized (mListeners) {
1279                for (Result listener : mListeners) {
1280                    listener.sendMailCallback(me, accountId, messageId, 0);
1281                }
1282            }
1283        }
1284
1285        @Override
1286        public void messageRetrieved(com.android.email.mail.Message message) {
1287        }
1288    }
1289
1290    /**
1291     * Service callback for service operations
1292     */
1293    private class ServiceCallback extends IEmailServiceCallback.Stub {
1294
1295        private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
1296
1297        public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
1298                int progress) {
1299            MessagingException result = mapStatusToException(statusCode);
1300            switch (statusCode) {
1301                case EmailServiceStatus.SUCCESS:
1302                    progress = 100;
1303                    break;
1304                case EmailServiceStatus.IN_PROGRESS:
1305                    if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
1306                        result = new MessagingException(
1307                                String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
1308                    }
1309                    // discard progress reports that look like sentinels
1310                    if (progress < 0 || progress >= 100) {
1311                        return;
1312                    }
1313                    break;
1314            }
1315            synchronized (mListeners) {
1316                for (Result listener : mListeners) {
1317                    listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
1318                }
1319            }
1320        }
1321
1322        /**
1323         * Note, this is an incomplete implementation of this callback, because we are
1324         * not getting things back from Service in quite the same way as from MessagingController.
1325         * However, this is sufficient for basic "progress=100" notification that message send
1326         * has just completed.
1327         */
1328        public void sendMessageStatus(long messageId, String subject, int statusCode,
1329                int progress) {
1330            long accountId = -1;        // This should be in the callback
1331            MessagingException result = mapStatusToException(statusCode);
1332            switch (statusCode) {
1333                case EmailServiceStatus.SUCCESS:
1334                    progress = 100;
1335                    break;
1336                case EmailServiceStatus.IN_PROGRESS:
1337                    // discard progress reports that look like sentinels
1338                    if (progress < 0 || progress >= 100) {
1339                        return;
1340                    }
1341                    break;
1342            }
1343            synchronized(mListeners) {
1344                for (Result listener : mListeners) {
1345                    listener.sendMailCallback(result, accountId, messageId, progress);
1346                }
1347            }
1348        }
1349
1350        public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
1351            MessagingException result = mapStatusToException(statusCode);
1352            switch (statusCode) {
1353                case EmailServiceStatus.SUCCESS:
1354                    progress = 100;
1355                    break;
1356                case EmailServiceStatus.IN_PROGRESS:
1357                    // discard progress reports that look like sentinels
1358                    if (progress < 0 || progress >= 100) {
1359                        return;
1360                    }
1361                    break;
1362            }
1363            synchronized(mListeners) {
1364                for (Result listener : mListeners) {
1365                    listener.updateMailboxListCallback(result, accountId, progress);
1366                }
1367            }
1368        }
1369
1370        public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
1371            MessagingException result = mapStatusToException(statusCode);
1372            switch (statusCode) {
1373                case EmailServiceStatus.SUCCESS:
1374                    progress = 100;
1375                    break;
1376                case EmailServiceStatus.IN_PROGRESS:
1377                    // discard progress reports that look like sentinels
1378                    if (progress < 0 || progress >= 100) {
1379                        return;
1380                    }
1381                    break;
1382            }
1383            // TODO should pass this back instead of looking it up here
1384            Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
1385            // The mailbox could have disappeared if the server commanded it
1386            if (mbx == null) return;
1387            long accountId = mbx.mAccountKey;
1388            synchronized(mListeners) {
1389                for (Result listener : mListeners) {
1390                    listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
1391                }
1392            }
1393        }
1394
1395        private MessagingException mapStatusToException(int statusCode) {
1396            switch (statusCode) {
1397                case EmailServiceStatus.SUCCESS:
1398                case EmailServiceStatus.IN_PROGRESS:
1399                    return null;
1400
1401                case EmailServiceStatus.LOGIN_FAILED:
1402                    return new AuthenticationFailedException("");
1403
1404                case EmailServiceStatus.CONNECTION_ERROR:
1405                    return new MessagingException(MessagingException.IOERROR);
1406
1407                case EmailServiceStatus.SECURITY_FAILURE:
1408                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
1409
1410                case EmailServiceStatus.MESSAGE_NOT_FOUND:
1411                case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
1412                case EmailServiceStatus.FOLDER_NOT_DELETED:
1413                case EmailServiceStatus.FOLDER_NOT_RENAMED:
1414                case EmailServiceStatus.FOLDER_NOT_CREATED:
1415                case EmailServiceStatus.REMOTE_EXCEPTION:
1416                    // TODO: define exception code(s) & UI string(s) for server-side errors
1417                default:
1418                    return new MessagingException(String.valueOf(statusCode));
1419            }
1420        }
1421    }
1422
1423    private interface ServiceCallbackWrapper {
1424        public void call(IEmailServiceCallback cb) throws RemoteException;
1425    }
1426
1427    /**
1428     * Proxy that can be used to broadcast service callbacks; we currently use this only for
1429     * loadAttachment callbacks
1430     */
1431    private final IEmailServiceCallback.Stub mCallbackProxy =
1432        new IEmailServiceCallback.Stub() {
1433
1434        /**
1435         * Broadcast a callback to the everyone that's registered
1436         *
1437         * @param wrapper the ServiceCallbackWrapper used in the broadcast
1438         */
1439        private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
1440            if (sCallbackList != null) {
1441                // Call everyone on our callback list
1442                // Exceptions can be safely ignored
1443                int count = sCallbackList.beginBroadcast();
1444                for (int i = 0; i < count; i++) {
1445                    try {
1446                        wrapper.call(sCallbackList.getBroadcastItem(i));
1447                    } catch (RemoteException e) {
1448                    }
1449                }
1450                sCallbackList.finishBroadcast();
1451            }
1452        }
1453
1454        public void loadAttachmentStatus(final long messageId, final long attachmentId,
1455                final int status, final int progress) {
1456            broadcastCallback(new ServiceCallbackWrapper() {
1457                @Override
1458                public void call(IEmailServiceCallback cb) throws RemoteException {
1459                    cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
1460                }
1461            });
1462        }
1463
1464        @Override
1465        public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
1466                throws RemoteException {
1467        }
1468
1469        @Override
1470        public void syncMailboxListStatus(long accountId, int statusCode, int progress)
1471                throws RemoteException {
1472        }
1473
1474        @Override
1475        public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
1476                throws RemoteException {
1477        }
1478    };
1479
1480    public static class ControllerService extends Service {
1481        /**
1482         * Create our EmailService implementation here.  For now, only loadAttachment is supported; the
1483         * intention, however, is to move more functionality to the service interface
1484         */
1485        private final IEmailService.Stub mBinder = new IEmailService.Stub() {
1486
1487            public Bundle validate(String protocol, String host, String userName, String password,
1488                    int port, boolean ssl, boolean trustCertificates) throws RemoteException {
1489                return null;
1490            }
1491
1492            public Bundle autoDiscover(String userName, String password) throws RemoteException {
1493                return null;
1494            }
1495
1496            public void startSync(long mailboxId) throws RemoteException {
1497            }
1498
1499            public void stopSync(long mailboxId) throws RemoteException {
1500            }
1501
1502            public void loadAttachment(long attachmentId, String destinationFile,
1503                    String contentUriString) throws RemoteException {
1504                if (Email.DEBUG) {
1505                    Log.d(TAG, "loadAttachment: " + attachmentId + " to " + destinationFile);
1506                }
1507                Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this,
1508                        attachmentId);
1509                if (att != null) {
1510                    Message msg = Message.restoreMessageWithId(ControllerService.this,
1511                            att.mMessageKey);
1512                    if (msg != null) {
1513                        // If the message is a forward and the attachment needs downloading, we need
1514                        // to retrieve the message from the source, rather than from the message
1515                        // itself
1516                        if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
1517                            String[] cols = Utility.getRowColumns(ControllerService.this,
1518                                    Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY,
1519                                    new String[] {Long.toString(msg.mId)});
1520                            if (cols != null) {
1521                                msg = Message.restoreMessageWithId(ControllerService.this,
1522                                        Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN]));
1523                                if (msg == null) {
1524                                    // TODO: We can try restoring from the deleted table at this point...
1525                                    return;
1526                                }
1527                            }
1528                        }
1529                        MessagingController legacyController = sInstance.mLegacyController;
1530                        LegacyListener legacyListener = sInstance.mLegacyListener;
1531                        legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey,
1532                                attachmentId, legacyListener);
1533                    }
1534                }
1535            }
1536
1537            public void updateFolderList(long accountId) throws RemoteException {
1538            }
1539
1540            public void hostChanged(long accountId) throws RemoteException {
1541            }
1542
1543            public void setLogging(int on) throws RemoteException {
1544            }
1545
1546            public void sendMeetingResponse(long messageId, int response) throws RemoteException {
1547            }
1548
1549            public void loadMore(long messageId) throws RemoteException {
1550            }
1551
1552            // The following three methods are not implemented in this version
1553            public boolean createFolder(long accountId, String name) throws RemoteException {
1554                return false;
1555            }
1556
1557            public boolean deleteFolder(long accountId, String name) throws RemoteException {
1558                return false;
1559            }
1560
1561            public boolean renameFolder(long accountId, String oldName, String newName)
1562                    throws RemoteException {
1563                return false;
1564            }
1565
1566            public void setCallback(IEmailServiceCallback cb) throws RemoteException {
1567                sCallbackList.register(cb);
1568            }
1569
1570            public void moveMessage(long messageId, long mailboxId) throws RemoteException {
1571            }
1572        };
1573
1574        @Override
1575        public IBinder onBind(Intent intent) {
1576            return mBinder;
1577        }
1578    }
1579}
1580