Home | History | Annotate | Download | only in email
      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 
     17 package com.android.email;
     18 
     19 import android.app.Service;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Bundle;
     28 import android.os.IBinder;
     29 import android.os.RemoteCallbackList;
     30 import android.os.RemoteException;
     31 import android.util.Log;
     32 
     33 import com.android.email.mail.store.Pop3Store.Pop3Message;
     34 import com.android.email.provider.AccountBackupRestore;
     35 import com.android.email.service.EmailServiceUtils;
     36 import com.android.email.service.MailService;
     37 import com.android.emailcommon.Api;
     38 import com.android.emailcommon.Logging;
     39 import com.android.emailcommon.mail.AuthenticationFailedException;
     40 import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
     41 import com.android.emailcommon.mail.MessagingException;
     42 import com.android.emailcommon.provider.Account;
     43 import com.android.emailcommon.provider.EmailContent;
     44 import com.android.emailcommon.provider.EmailContent.Attachment;
     45 import com.android.emailcommon.provider.EmailContent.Body;
     46 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     47 import com.android.emailcommon.provider.EmailContent.Message;
     48 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     49 import com.android.emailcommon.provider.HostAuth;
     50 import com.android.emailcommon.provider.Mailbox;
     51 import com.android.emailcommon.service.EmailServiceStatus;
     52 import com.android.emailcommon.service.IEmailService;
     53 import com.android.emailcommon.service.IEmailServiceCallback;
     54 import com.android.emailcommon.service.SearchParams;
     55 import com.android.emailcommon.utility.AttachmentUtilities;
     56 import com.android.emailcommon.utility.EmailAsyncTask;
     57 import com.android.emailcommon.utility.Utility;
     58 import com.google.common.annotations.VisibleForTesting;
     59 
     60 import java.io.FileNotFoundException;
     61 import java.io.IOException;
     62 import java.io.InputStream;
     63 import java.util.ArrayList;
     64 import java.util.Collection;
     65 import java.util.HashMap;
     66 import java.util.HashSet;
     67 import java.util.concurrent.ConcurrentHashMap;
     68 
     69 /**
     70  * New central controller/dispatcher for Email activities that may require remote operations.
     71  * Handles disambiguating between legacy MessagingController operations and newer provider/sync
     72  * based code.  We implement Service to allow loadAttachment calls to be sent in a consistent manner
     73  * to IMAP, POP3, and EAS by AttachmentDownloadService
     74  */
     75 public class Controller {
     76     private static final String TAG = "Controller";
     77     private static Controller sInstance;
     78     private final Context mContext;
     79     private Context mProviderContext;
     80     private final MessagingController mLegacyController;
     81     private final LegacyListener mLegacyListener = new LegacyListener();
     82     private final ServiceCallback mServiceCallback = new ServiceCallback();
     83     private final HashSet<Result> mListeners = new HashSet<Result>();
     84     /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap =
     85         new ConcurrentHashMap<Long, Boolean>();
     86 
     87     // Note that 0 is a syntactically valid account key; however there can never be an account
     88     // with id = 0, so attempts to restore the account will return null.  Null values are
     89     // handled properly within the code, so this won't cause any issues.
     90     private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0;
     91     /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__";
     92     /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__";
     93     /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__";
     94     private static final String WHERE_TYPE_ATTACHMENT =
     95         MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT;
     96     private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?";
     97 
     98     private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] {
     99         EmailContent.RECORD_ID,
    100         EmailContent.MessageColumns.ACCOUNT_KEY
    101     };
    102     private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
    103 
    104     private static final String[] BODY_SOURCE_KEY_PROJECTION =
    105         new String[] {Body.SOURCE_MESSAGE_KEY};
    106     private static final int BODY_SOURCE_KEY_COLUMN = 0;
    107     private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
    108 
    109     private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
    110     private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION =
    111         MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" +
    112         Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
    113     private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?";
    114 
    115     // Service callbacks as set up via setCallback
    116     private static RemoteCallbackList<IEmailServiceCallback> sCallbackList =
    117         new RemoteCallbackList<IEmailServiceCallback>();
    118 
    119     private volatile boolean mInUnitTests = false;
    120 
    121     protected Controller(Context _context) {
    122         mContext = _context.getApplicationContext();
    123         mProviderContext = _context;
    124         mLegacyController = MessagingController.getInstance(mProviderContext, this);
    125         mLegacyController.addListener(mLegacyListener);
    126     }
    127 
    128     /**
    129      * Mark this controller as being in use in a unit test.
    130      * This is a kludge vs having proper mocks and dependency injection; since the Controller is a
    131      * global singleton there isn't much else we can do.
    132      */
    133     public void markForTest(boolean inUnitTests) {
    134         mInUnitTests = inUnitTests;
    135     }
    136 
    137     /**
    138      * Cleanup for test.  Mustn't be called for the regular {@link Controller}, as it's a
    139      * singleton and lives till the process finishes.
    140      *
    141      * <p>However, this method MUST be called for mock instances.
    142      */
    143     public void cleanupForTest() {
    144         mLegacyController.removeListener(mLegacyListener);
    145     }
    146 
    147     /**
    148      * Gets or creates the singleton instance of Controller.
    149      */
    150     public synchronized static Controller getInstance(Context _context) {
    151         if (sInstance == null) {
    152             sInstance = new Controller(_context);
    153         }
    154         return sInstance;
    155     }
    156 
    157     /**
    158      * Inject a mock controller.  Used only for testing.  Affects future calls to getInstance().
    159      *
    160      * Tests that use this method MUST clean it up by calling this method again with null.
    161      */
    162     public synchronized static void injectMockControllerForTest(Controller mockController) {
    163         sInstance = mockController;
    164     }
    165 
    166     /**
    167      * For testing only:  Inject a different context for provider access.  This will be
    168      * used internally for access the underlying provider (e.g. getContentResolver().query()).
    169      * @param providerContext the provider context to be used by this instance
    170      */
    171     public void setProviderContext(Context providerContext) {
    172         mProviderContext = providerContext;
    173     }
    174 
    175     /**
    176      * Any UI code that wishes for callback results (on async ops) should register their callback
    177      * here (typically from onResume()).  Unregistered callbacks will never be called, to prevent
    178      * problems when the command completes and the activity has already paused or finished.
    179      * @param listener The callback that may be used in action methods
    180      */
    181     public void addResultCallback(Result listener) {
    182         synchronized (mListeners) {
    183             listener.setRegistered(true);
    184             mListeners.add(listener);
    185         }
    186     }
    187 
    188     /**
    189      * Any UI code that no longer wishes for callback results (on async ops) should unregister
    190      * their callback here (typically from onPause()).  Unregistered callbacks will never be called,
    191      * to prevent problems when the command completes and the activity has already paused or
    192      * finished.
    193      * @param listener The callback that may no longer be used
    194      */
    195     public void removeResultCallback(Result listener) {
    196         synchronized (mListeners) {
    197             listener.setRegistered(false);
    198             mListeners.remove(listener);
    199         }
    200     }
    201 
    202     public Collection<Result> getResultCallbacksForTest() {
    203         return mListeners;
    204     }
    205 
    206     /**
    207      * Delete all Messages that live in the attachment mailbox
    208      */
    209     public void deleteAttachmentMessages() {
    210         // Note: There should only be one attachment mailbox at present
    211         ContentResolver resolver = mProviderContext.getContentResolver();
    212         Cursor c = null;
    213         try {
    214             c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
    215                     WHERE_TYPE_ATTACHMENT, null, null);
    216             while (c.moveToNext()) {
    217                 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
    218                 // Must delete attachments BEFORE messages
    219                 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0,
    220                         mailboxId);
    221                 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY,
    222                         new String[] {Long.toString(mailboxId)});
    223            }
    224         } finally {
    225             if (c != null) {
    226                 c.close();
    227             }
    228         }
    229     }
    230 
    231     /**
    232      * Get a mailbox based on a sqlite WHERE clause
    233      */
    234     private Mailbox getGlobalMailboxWhere(String where) {
    235         Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI,
    236                 Mailbox.CONTENT_PROJECTION, where, null, null);
    237         try {
    238             if (c.moveToFirst()) {
    239                 Mailbox m = new Mailbox();
    240                 m.restore(c);
    241                 return m;
    242             }
    243         } finally {
    244             c.close();
    245         }
    246         return null;
    247     }
    248 
    249     /**
    250      * Returns the attachment mailbox (where we store eml attachment Emails), creating one
    251      * if necessary
    252      * @return the global attachment mailbox
    253      */
    254     public Mailbox getAttachmentMailbox() {
    255         Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT);
    256         if (m == null) {
    257             m = new Mailbox();
    258             m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY;
    259             m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID;
    260             m.mFlagVisible = false;
    261             m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID;
    262             m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
    263             m.mType = Mailbox.TYPE_ATTACHMENT;
    264             m.save(mProviderContext);
    265         }
    266         return m;
    267     }
    268 
    269     /**
    270      * Returns the search mailbox for the specified account, creating one if necessary
    271      * @return the search mailbox for the passed in account
    272      */
    273     public Mailbox getSearchMailbox(long accountId) {
    274         Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH);
    275         if (m == null) {
    276             m = new Mailbox();
    277             m.mAccountKey = accountId;
    278             m.mServerId = SEARCH_MAILBOX_SERVER_ID;
    279             m.mFlagVisible = false;
    280             m.mDisplayName = SEARCH_MAILBOX_SERVER_ID;
    281             m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
    282             m.mType = Mailbox.TYPE_SEARCH;
    283             m.mFlags = Mailbox.FLAG_HOLDS_MAIL;
    284             m.mParentKey = Mailbox.NO_MAILBOX;
    285             m.save(mProviderContext);
    286         }
    287         return m;
    288     }
    289 
    290     /**
    291      * Create a Message from the Uri and store it in the attachment mailbox
    292      * @param uri the uri containing message content
    293      * @return the Message or null
    294      */
    295     public Message loadMessageFromUri(Uri uri) {
    296         Mailbox mailbox = getAttachmentMailbox();
    297         if (mailbox == null) return null;
    298         try {
    299             InputStream is = mProviderContext.getContentResolver().openInputStream(uri);
    300             try {
    301                 // First, create a Pop3Message from the attachment and then parse it
    302                 Pop3Message pop3Message = new Pop3Message(
    303                         ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null);
    304                 pop3Message.parse(is);
    305                 // Now, pull out the header fields
    306                 Message msg = new Message();
    307                 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId);
    308                 // Commit the message to the local store
    309                 msg.save(mProviderContext);
    310                 // Setup the rest of the message and mark it completely loaded
    311                 mLegacyController.copyOneMessageToProvider(pop3Message, msg,
    312                         Message.FLAG_LOADED_COMPLETE, mProviderContext);
    313                 // Restore the complete message and return it
    314                 return Message.restoreMessageWithId(mProviderContext, msg.mId);
    315             } catch (MessagingException e) {
    316             } catch (IOException e) {
    317             }
    318         } catch (FileNotFoundException e) {
    319         }
    320         return null;
    321     }
    322 
    323     /**
    324      * Set logging flags for external sync services
    325      *
    326      * Generally this should be called by anybody who changes Email.DEBUG
    327      */
    328     public void serviceLogging(int debugFlags) {
    329         IEmailService service = EmailServiceUtils.getExchangeService(mContext, mServiceCallback);
    330         try {
    331             service.setLogging(debugFlags);
    332         } catch (RemoteException e) {
    333             // TODO Change exception handling to be consistent with however this method
    334             // is implemented for other protocols
    335             Log.d("setLogging", "RemoteException" + e);
    336         }
    337     }
    338 
    339     /**
    340      * Request a remote update of mailboxes for an account.
    341      */
    342     public void updateMailboxList(final long accountId) {
    343         Utility.runAsync(new Runnable() {
    344             @Override
    345             public void run() {
    346                 final IEmailService service = getServiceForAccount(accountId);
    347                 if (service != null) {
    348                     // Service implementation
    349                     try {
    350                         service.updateFolderList(accountId);
    351                     } catch (RemoteException e) {
    352                         // TODO Change exception handling to be consistent with however this method
    353                         // is implemented for other protocols
    354                         Log.d("updateMailboxList", "RemoteException" + e);
    355                     }
    356                 } else {
    357                     // MessagingController implementation
    358                     mLegacyController.listFolders(accountId, mLegacyListener);
    359                 }
    360             }
    361         });
    362     }
    363 
    364     /**
    365      * Request a remote update of a mailbox.  For use by the timed service.
    366      *
    367      * Functionally this is quite similar to updateMailbox(), but it's a separate API and
    368      * separate callback in order to keep UI callbacks from affecting the service loop.
    369      */
    370     public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) {
    371         IEmailService service = getServiceForAccount(accountId);
    372         if (service != null) {
    373             // Service implementation
    374 //            try {
    375                 // TODO this isn't quite going to work, because we're going to get the
    376                 // generic (UI) callbacks and not the ones we need to restart the ol' service.
    377                 // service.startSync(mailboxId, tag);
    378             mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag);
    379 //            } catch (RemoteException e) {
    380                 // TODO Change exception handling to be consistent with however this method
    381                 // is implemented for other protocols
    382 //                Log.d("updateMailbox", "RemoteException" + e);
    383 //            }
    384         } else {
    385             // MessagingController implementation
    386             Utility.runAsync(new Runnable() {
    387                 public void run() {
    388                     mLegacyController.checkMail(accountId, tag, mLegacyListener);
    389                 }
    390             });
    391         }
    392     }
    393 
    394     /**
    395      * Request a remote update of a mailbox.
    396      *
    397      * The contract here should be to try and update the headers ASAP, in order to populate
    398      * a simple message list.  We should also at this point queue up a background task of
    399      * downloading some/all of the messages in this mailbox, but that should be interruptable.
    400      */
    401     public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) {
    402 
    403         IEmailService service = getServiceForAccount(accountId);
    404         if (service != null) {
    405            try {
    406                 service.startSync(mailboxId, userRequest);
    407             } catch (RemoteException e) {
    408                 // TODO Change exception handling to be consistent with however this method
    409                 // is implemented for other protocols
    410                 Log.d("updateMailbox", "RemoteException" + e);
    411             }
    412         } else {
    413             // MessagingController implementation
    414             Utility.runAsync(new Runnable() {
    415                 public void run() {
    416                     // TODO shouldn't be passing fully-build accounts & mailboxes into APIs
    417                     Account account =
    418                         Account.restoreAccountWithId(mProviderContext, accountId);
    419                     Mailbox mailbox =
    420                         Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
    421                     if (account == null || mailbox == null ||
    422                             mailbox.mType == Mailbox.TYPE_SEARCH) {
    423                         return;
    424                     }
    425                     mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
    426                 }
    427             });
    428         }
    429     }
    430 
    431     /**
    432      * Request that any final work necessary be done, to load a message.
    433      *
    434      * Note, this assumes that the caller has already checked message.mFlagLoaded and that
    435      * additional work is needed.  There is no optimization here for a message which is already
    436      * loaded.
    437      *
    438      * @param messageId the message to load
    439      * @param callback the Controller callback by which results will be reported
    440      */
    441     public void loadMessageForView(final long messageId) {
    442 
    443         // Split here for target type (Service or MessagingController)
    444         IEmailService service = getServiceForMessage(messageId);
    445         if (service != null) {
    446             // There is no service implementation, so we'll just jam the value, log the error,
    447             // and get out of here.
    448             Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
    449             ContentValues cv = new ContentValues();
    450             cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
    451             mProviderContext.getContentResolver().update(uri, cv, null, null);
    452             Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
    453             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
    454             synchronized (mListeners) {
    455                 for (Result listener : mListeners) {
    456                     listener.loadMessageForViewCallback(null, accountId, messageId, 100);
    457                 }
    458             }
    459         } else {
    460             // MessagingController implementation
    461             Utility.runAsync(new Runnable() {
    462                 public void run() {
    463                     mLegacyController.loadMessageForView(messageId, mLegacyListener);
    464                 }
    465             });
    466         }
    467     }
    468 
    469 
    470     /**
    471      * Saves the message to a mailbox of given type.
    472      * This is a synchronous operation taking place in the same thread as the caller.
    473      * Upon return the message.mId is set.
    474      * @param message the message (must have the mAccountId set).
    475      * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS).
    476      */
    477     public void saveToMailbox(final EmailContent.Message message, final int mailboxType) {
    478         long accountId = message.mAccountKey;
    479         long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType);
    480         message.mMailboxKey = mailboxId;
    481         message.save(mProviderContext);
    482     }
    483 
    484     /**
    485      * Look for a specific system mailbox, creating it if necessary, and return the mailbox id.
    486      * This is a blocking operation and should not be called from the UI thread.
    487      *
    488      * Synchronized so multiple threads can call it (and not risk creating duplicate boxes).
    489      *
    490      * @param accountId the account id
    491      * @param mailboxType the mailbox type (e.g.  EmailContent.Mailbox.TYPE_TRASH)
    492      * @return the id of the mailbox. The mailbox is created if not existing.
    493      * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative.
    494      * Does not validate the input in other ways (e.g. does not verify the existence of account).
    495      */
    496     public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) {
    497         if (accountId < 0 || mailboxType < 0) {
    498             return Mailbox.NO_MAILBOX;
    499         }
    500         long mailboxId =
    501             Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType);
    502         return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId;
    503     }
    504 
    505     /**
    506      * Returns the server-side name for a specific mailbox.
    507      *
    508      * @return the resource string corresponding to the mailbox type, empty if not found.
    509      */
    510     public static String getMailboxServerName(Context context, int mailboxType) {
    511         int resId = -1;
    512         switch (mailboxType) {
    513             case Mailbox.TYPE_INBOX:
    514                 resId = R.string.mailbox_name_server_inbox;
    515                 break;
    516             case Mailbox.TYPE_OUTBOX:
    517                 resId = R.string.mailbox_name_server_outbox;
    518                 break;
    519             case Mailbox.TYPE_DRAFTS:
    520                 resId = R.string.mailbox_name_server_drafts;
    521                 break;
    522             case Mailbox.TYPE_TRASH:
    523                 resId = R.string.mailbox_name_server_trash;
    524                 break;
    525             case Mailbox.TYPE_SENT:
    526                 resId = R.string.mailbox_name_server_sent;
    527                 break;
    528             case Mailbox.TYPE_JUNK:
    529                 resId = R.string.mailbox_name_server_junk;
    530                 break;
    531         }
    532         return resId != -1 ? context.getString(resId) : "";
    533     }
    534 
    535     /**
    536      * Create a mailbox given the account and mailboxType.
    537      * TODO: Does this need to be signaled explicitly to the sync engines?
    538      */
    539     @VisibleForTesting
    540     long createMailbox(long accountId, int mailboxType) {
    541         if (accountId < 0 || mailboxType < 0) {
    542             String mes = "Invalid arguments " + accountId + ' ' + mailboxType;
    543             Log.e(Logging.LOG_TAG, mes);
    544             throw new RuntimeException(mes);
    545         }
    546         Mailbox box = Mailbox.newSystemMailbox(
    547                 accountId, mailboxType, getMailboxServerName(mContext, mailboxType));
    548         box.save(mProviderContext);
    549         return box.mId;
    550     }
    551 
    552     /**
    553      * Send a message:
    554      * - move the message to Outbox (the message is assumed to be in Drafts).
    555      * - EAS service will take it from there
    556      * - mark reply/forward state in source message (if any)
    557      * - trigger send for POP/IMAP
    558      * @param message the fully populated Message (usually retrieved from the Draft box). Note that
    559      *     all transient fields (e.g. Body related fields) are also expected to be fully loaded
    560      */
    561     public void sendMessage(Message message) {
    562         ContentResolver resolver = mProviderContext.getContentResolver();
    563         long accountId = message.mAccountKey;
    564         long messageId = message.mId;
    565         if (accountId == Account.NO_ACCOUNT) {
    566             accountId = lookupAccountForMessage(messageId);
    567         }
    568         if (accountId == Account.NO_ACCOUNT) {
    569             // probably the message was not found
    570             if (Logging.LOGD) {
    571                 Email.log("no account found for message " + messageId);
    572             }
    573             return;
    574         }
    575 
    576         // Move to Outbox
    577         long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX);
    578         ContentValues cv = new ContentValues();
    579         cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId);
    580 
    581         // does this need to be SYNCED_CONTENT_URI instead?
    582         Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
    583         resolver.update(uri, cv, null, null);
    584 
    585         // If this is a reply/forward, indicate it as such on the source.
    586         long sourceKey = message.mSourceKey;
    587         if (sourceKey != Message.NO_MESSAGE) {
    588             boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0;
    589             int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED;
    590             setMessageAnsweredOrForwarded(sourceKey, flagUpdate);
    591         }
    592 
    593         sendPendingMessages(accountId);
    594     }
    595 
    596     private void sendPendingMessagesSmtp(long accountId) {
    597         // for IMAP & POP only, (attempt to) send the message now
    598         final Account account =
    599                 Account.restoreAccountWithId(mProviderContext, accountId);
    600         if (account == null) {
    601             return;
    602         }
    603         final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
    604         Utility.runAsync(new Runnable() {
    605             public void run() {
    606                 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
    607             }
    608         });
    609     }
    610 
    611     /**
    612      * Try to send all pending messages for a given account
    613      *
    614      * @param accountId the account for which to send messages
    615      */
    616     public void sendPendingMessages(long accountId) {
    617         // 1. make sure we even have an outbox, exit early if not
    618         final long outboxId =
    619             Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
    620         if (outboxId == Mailbox.NO_MAILBOX) {
    621             return;
    622         }
    623 
    624         // 2. dispatch as necessary
    625         IEmailService service = getServiceForAccount(accountId);
    626         if (service != null) {
    627             // Service implementation
    628             try {
    629                 service.startSync(outboxId, false);
    630             } catch (RemoteException e) {
    631                 // TODO Change exception handling to be consistent with however this method
    632                 // is implemented for other protocols
    633                 Log.d("updateMailbox", "RemoteException" + e);
    634             }
    635         } else {
    636             // MessagingController implementation
    637             sendPendingMessagesSmtp(accountId);
    638         }
    639     }
    640 
    641     /**
    642      * Reset visible limits for all accounts.
    643      * For each account:
    644      *   look up limit
    645      *   write limit into all mailboxes for that account
    646      */
    647     public void resetVisibleLimits() {
    648         Utility.runAsync(new Runnable() {
    649             public void run() {
    650                 ContentResolver resolver = mProviderContext.getContentResolver();
    651                 Cursor c = null;
    652                 try {
    653                     c = resolver.query(
    654                             Account.CONTENT_URI,
    655                             Account.ID_PROJECTION,
    656                             null, null, null);
    657                     while (c.moveToNext()) {
    658                         long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
    659                         String protocol = Account.getProtocol(mProviderContext, accountId);
    660                         if (!HostAuth.SCHEME_EAS.equals(protocol)) {
    661                             ContentValues cv = new ContentValues();
    662                             cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT);
    663                             resolver.update(Mailbox.CONTENT_URI, cv,
    664                                     MailboxColumns.ACCOUNT_KEY + "=?",
    665                                     new String[] { Long.toString(accountId) });
    666                         }
    667                     }
    668                 } finally {
    669                     if (c != null) {
    670                         c.close();
    671                     }
    672                 }
    673             }
    674         });
    675     }
    676 
    677     /**
    678      * Increase the load count for a given mailbox, and trigger a refresh.  Applies only to
    679      * IMAP and POP mailboxes, with the exception of the EAS search mailbox.
    680      *
    681      * @param mailboxId the mailbox
    682      */
    683     public void loadMoreMessages(final long mailboxId) {
    684         EmailAsyncTask.runAsyncParallel(new Runnable() {
    685             public void run() {
    686                 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
    687                 if (mailbox == null) {
    688                     return;
    689                 }
    690                 if (mailbox.mType == Mailbox.TYPE_SEARCH) {
    691                     try {
    692                         searchMore(mailbox.mAccountKey);
    693                     } catch (MessagingException e) {
    694                         // Nothing to be done
    695                     }
    696                     return;
    697                 }
    698                 Account account = Account.restoreAccountWithId(mProviderContext,
    699                         mailbox.mAccountKey);
    700                 if (account == null) {
    701                     return;
    702                 }
    703                 // Use provider math to increment the field
    704                 ContentValues cv = new ContentValues();;
    705                 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT);
    706                 cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT);
    707                 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId);
    708                 mProviderContext.getContentResolver().update(uri, cv, null, null);
    709                 // Trigger a refresh using the new, longer limit
    710                 mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT;
    711                 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
    712             }
    713         });
    714     }
    715 
    716     /**
    717      * @param messageId the id of message
    718      * @return the accountId corresponding to the given messageId, or -1 if not found.
    719      */
    720     private long lookupAccountForMessage(long messageId) {
    721         ContentResolver resolver = mProviderContext.getContentResolver();
    722         Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
    723                                   MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?",
    724                                   new String[] { Long.toString(messageId) }, null);
    725         try {
    726             return c.moveToFirst()
    727                 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID)
    728                 : -1;
    729         } finally {
    730             c.close();
    731         }
    732     }
    733 
    734     /**
    735      * Delete a single attachment entry from the DB given its id.
    736      * Does not delete any eventual associated files.
    737      */
    738     public void deleteAttachment(long attachmentId) {
    739         ContentResolver resolver = mProviderContext.getContentResolver();
    740         Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
    741         resolver.delete(uri, null, null);
    742     }
    743 
    744     /**
    745      * Async version of {@link #deleteMessageSync}.
    746      */
    747     public void deleteMessage(final long messageId) {
    748         EmailAsyncTask.runAsyncParallel(new Runnable() {
    749             public void run() {
    750                 deleteMessageSync(messageId);
    751             }
    752         });
    753     }
    754 
    755     /**
    756      * Batch & async version of {@link #deleteMessageSync}.
    757      */
    758     public void deleteMessages(final long[] messageIds) {
    759         if (messageIds == null || messageIds.length == 0) {
    760             throw new IllegalArgumentException();
    761         }
    762         EmailAsyncTask.runAsyncParallel(new Runnable() {
    763             public void run() {
    764                 for (long messageId: messageIds) {
    765                     deleteMessageSync(messageId);
    766                 }
    767             }
    768         });
    769     }
    770 
    771     /**
    772      * Delete a single message by moving it to the trash, or really delete it if it's already in
    773      * trash or a draft message.
    774      *
    775      * This function has no callback, no result reporting, because the desired outcome
    776      * is reflected entirely by changes to one or more cursors.
    777      *
    778      * @param messageId The id of the message to "delete".
    779      */
    780     /* package */ void deleteMessageSync(long messageId) {
    781         // 1. Get the message's account
    782         Account account = Account.getAccountForMessageId(mProviderContext, messageId);
    783 
    784         if (account == null) return;
    785 
    786         // 2. Confirm that there is a trash mailbox available.  If not, create one
    787         long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH);
    788 
    789         // 3. Get the message's original mailbox
    790         Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId);
    791 
    792         if (mailbox == null) return;
    793 
    794         // 4.  Drop non-essential data for the message (e.g. attachment files)
    795         AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId,
    796                 messageId);
    797 
    798         Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI,
    799                 messageId);
    800         ContentResolver resolver = mProviderContext.getContentResolver();
    801 
    802         // 5. Perform "delete" as appropriate
    803         if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) {
    804             // 5a. Really delete it
    805             resolver.delete(uri, null, null);
    806         } else {
    807             // 5b. Move to trash
    808             ContentValues cv = new ContentValues();
    809             cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId);
    810             resolver.update(uri, cv, null, null);
    811         }
    812 
    813         if (isMessagingController(account)) {
    814             mLegacyController.processPendingActions(account.mId);
    815         }
    816     }
    817 
    818     /**
    819      * Moves messages to a new mailbox.
    820      *
    821      * This function has no callback, no result reporting, because the desired outcome
    822      * is reflected entirely by changes to one or more cursors.
    823      *
    824      * Note this method assumes all of the given message and mailbox IDs belong to the same
    825      * account.
    826      *
    827      * @param messageIds IDs of the messages that are to be moved
    828      * @param newMailboxId ID of the new mailbox that the messages will be moved to
    829      * @return an asynchronous task that executes the move (for testing only)
    830      */
    831     public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds,
    832             final long newMailboxId) {
    833         if (messageIds == null || messageIds.length == 0) {
    834             throw new IllegalArgumentException();
    835         }
    836         return EmailAsyncTask.runAsyncParallel(new Runnable() {
    837             public void run() {
    838                 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]);
    839                 if (account != null) {
    840                     ContentValues cv = new ContentValues();
    841                     cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId);
    842                     ContentResolver resolver = mProviderContext.getContentResolver();
    843                     for (long messageId : messageIds) {
    844                         Uri uri = ContentUris.withAppendedId(
    845                                 EmailContent.Message.SYNCED_CONTENT_URI, messageId);
    846                         resolver.update(uri, cv, null, null);
    847                     }
    848                     if (isMessagingController(account)) {
    849                         mLegacyController.processPendingActions(account.mId);
    850                     }
    851                 }
    852             }
    853         });
    854     }
    855 
    856     /**
    857      * Set/clear the unread status of a message
    858      *
    859      * @param messageId the message to update
    860      * @param isRead the new value for the isRead flag
    861      */
    862     public void setMessageReadSync(long messageId, boolean isRead) {
    863         setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
    864     }
    865 
    866     /**
    867      * Set/clear the unread status of a message from UI thread
    868      *
    869      * @param messageId the message to update
    870      * @param isRead the new value for the isRead flag
    871      * @return the EmailAsyncTask created
    872      */
    873     public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId,
    874             final boolean isRead) {
    875         return EmailAsyncTask.runAsyncParallel(new Runnable() {
    876             @Override
    877             public void run() {
    878                 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead);
    879             }});
    880     }
    881 
    882     /**
    883      * Update a message record and ping MessagingController, if necessary
    884      *
    885      * @param messageId the message to update
    886      * @param cv the ContentValues used in the update
    887      */
    888     private void updateMessageSync(long messageId, ContentValues cv) {
    889         Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
    890         mProviderContext.getContentResolver().update(uri, cv, null, null);
    891 
    892         // Service runs automatically, MessagingController needs a kick
    893         long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
    894         if (accountId == Account.NO_ACCOUNT) return;
    895         if (isMessagingController(accountId)) {
    896             mLegacyController.processPendingActions(accountId);
    897         }
    898     }
    899 
    900     /**
    901      * Set the answered status of a message
    902      *
    903      * @param messageId the message to update
    904      * @return the AsyncTask that will execute the changes (for testing only)
    905      */
    906     public void setMessageAnsweredOrForwarded(final long messageId,
    907             final int flag) {
    908         EmailAsyncTask.runAsyncParallel(new Runnable() {
    909             public void run() {
    910                 Message msg = Message.restoreMessageWithId(mProviderContext, messageId);
    911                 if (msg == null) {
    912                     Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward");
    913                     return;
    914                 }
    915                 ContentValues cv = new ContentValues();
    916                 cv.put(MessageColumns.FLAGS, msg.mFlags | flag);
    917                 updateMessageSync(messageId, cv);
    918             }
    919         });
    920     }
    921 
    922     /**
    923      * Set/clear the favorite status of a message from UI thread
    924      *
    925      * @param messageId the message to update
    926      * @param isFavorite the new value for the isFavorite flag
    927      * @return the EmailAsyncTask created
    928      */
    929     public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId,
    930             final boolean isFavorite) {
    931         return EmailAsyncTask.runAsyncParallel(new Runnable() {
    932             @Override
    933             public void run() {
    934                 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE,
    935                         isFavorite);
    936             }});
    937     }
    938     /**
    939      * Set/clear the favorite status of a message
    940      *
    941      * @param messageId the message to update
    942      * @param isFavorite the new value for the isFavorite flag
    943      */
    944     public void setMessageFavoriteSync(long messageId, boolean isFavorite) {
    945         setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite);
    946     }
    947 
    948     /**
    949      * Set/clear boolean columns of a message
    950      *
    951      * @param messageId the message to update
    952      * @param columnName the column to update
    953      * @param columnValue the new value for the column
    954      */
    955     private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) {
    956         ContentValues cv = new ContentValues();
    957         cv.put(columnName, columnValue);
    958         updateMessageSync(messageId, cv);
    959     }
    960 
    961 
    962     private static final HashMap<Long, SearchParams> sSearchParamsMap =
    963         new HashMap<Long, SearchParams>();
    964 
    965     public void searchMore(long accountId) throws MessagingException {
    966         SearchParams params = sSearchParamsMap.get(accountId);
    967         if (params == null) return;
    968         params.mOffset += params.mLimit;
    969         searchMessages(accountId, params);
    970     }
    971 
    972     /**
    973      * Search for messages on the (IMAP) server; do not call this on the UI thread!
    974      * @param accountId the id of the account to be searched
    975      * @param searchParams the parameters for this search
    976      * @throws MessagingException
    977      */
    978     public int searchMessages(final long accountId, final SearchParams searchParams)
    979             throws MessagingException {
    980         // Find/create our search mailbox
    981         Mailbox searchMailbox = getSearchMailbox(accountId);
    982         if (searchMailbox == null) return 0;
    983         final long searchMailboxId = searchMailbox.mId;
    984         // Save this away (per account)
    985         sSearchParamsMap.put(accountId, searchParams);
    986 
    987         if (searchParams.mOffset == 0) {
    988             // Delete existing contents of search mailbox
    989             ContentResolver resolver = mContext.getContentResolver();
    990             resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId,
    991                     null);
    992             ContentValues cv = new ContentValues();
    993             // For now, use the actual query as the name of the mailbox
    994             cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter);
    995             resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
    996                     cv, null, null);
    997         }
    998 
    999         IEmailService service = getServiceForAccount(accountId);
   1000         if (service != null) {
   1001             // Service implementation
   1002             try {
   1003                 return service.searchMessages(accountId, searchParams, searchMailboxId);
   1004             } catch (RemoteException e) {
   1005                 // TODO Change exception handling to be consistent with however this method
   1006                 // is implemented for other protocols
   1007                 Log.e("searchMessages", "RemoteException", e);
   1008                 return 0;
   1009             }
   1010         } else {
   1011             // This is the actual mailbox we'll be searching
   1012             Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId);
   1013             if (actualMailbox == null) {
   1014                 Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId
   1015                         + " to search in with " + searchParams);
   1016                 return 0;
   1017             }
   1018             // Do the search
   1019             if (Email.DEBUG) {
   1020                 Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter);
   1021             }
   1022             return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId);
   1023         }
   1024     }
   1025 
   1026     /**
   1027      * Respond to a meeting invitation.
   1028      *
   1029      * @param messageId the id of the invitation being responded to
   1030      * @param response the code representing the response to the invitation
   1031      */
   1032     public void sendMeetingResponse(final long messageId, final int response) {
   1033          // Split here for target type (Service or MessagingController)
   1034         IEmailService service = getServiceForMessage(messageId);
   1035         if (service != null) {
   1036             // Service implementation
   1037             try {
   1038                 service.sendMeetingResponse(messageId, response);
   1039             } catch (RemoteException e) {
   1040                 // TODO Change exception handling to be consistent with however this method
   1041                 // is implemented for other protocols
   1042                 Log.e("onDownloadAttachment", "RemoteException", e);
   1043             }
   1044         }
   1045     }
   1046 
   1047     /**
   1048      * Request that an attachment be loaded.  It will be stored at a location controlled
   1049      * by the AttachmentProvider.
   1050      *
   1051      * @param attachmentId the attachment to load
   1052      * @param messageId the owner message
   1053      * @param accountId the owner account
   1054      */
   1055     public void loadAttachment(final long attachmentId, final long messageId,
   1056             final long accountId) {
   1057         Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId);
   1058         if (attachInfo == null) {
   1059             return;
   1060         }
   1061 
   1062         if (Utility.attachmentExists(mProviderContext, attachInfo)) {
   1063             // The attachment has already been downloaded, so we will just "pretend" to download it
   1064             // This presumably is for POP3 messages
   1065             synchronized (mListeners) {
   1066                 for (Result listener : mListeners) {
   1067                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
   1068                 }
   1069                 for (Result listener : mListeners) {
   1070                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
   1071                 }
   1072             }
   1073             return;
   1074         }
   1075 
   1076         // Flag the attachment as needing download at the user's request
   1077         ContentValues cv = new ContentValues();
   1078         cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST);
   1079         attachInfo.update(mProviderContext, cv);
   1080     }
   1081 
   1082     /**
   1083      * For a given message id, return a service proxy if applicable, or null.
   1084      *
   1085      * @param messageId the message of interest
   1086      * @result service proxy, or null if n/a
   1087      */
   1088     private IEmailService getServiceForMessage(long messageId) {
   1089         // TODO make this more efficient, caching the account, smaller lookup here, etc.
   1090         Message message = Message.restoreMessageWithId(mProviderContext, messageId);
   1091         if (message == null) {
   1092             return null;
   1093         }
   1094         return getServiceForAccount(message.mAccountKey);
   1095     }
   1096 
   1097     /**
   1098      * For a given account id, return a service proxy if applicable, or null.
   1099      *
   1100      * @param accountId the message of interest
   1101      * @result service proxy, or null if n/a
   1102      */
   1103     private IEmailService getServiceForAccount(long accountId) {
   1104         if (isMessagingController(accountId)) return null;
   1105         return getExchangeEmailService();
   1106     }
   1107 
   1108     private IEmailService getExchangeEmailService() {
   1109         return EmailServiceUtils.getExchangeService(mContext, mServiceCallback);
   1110     }
   1111 
   1112     /**
   1113      * Simple helper to determine if legacy MessagingController should be used
   1114      */
   1115     public boolean isMessagingController(Account account) {
   1116         if (account == null) return false;
   1117         return isMessagingController(account.mId);
   1118     }
   1119 
   1120     public boolean isMessagingController(long accountId) {
   1121         Boolean isLegacyController = mLegacyControllerMap.get(accountId);
   1122         if (isLegacyController == null) {
   1123             String protocol = Account.getProtocol(mProviderContext, accountId);
   1124             isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol));
   1125             mLegacyControllerMap.put(accountId, isLegacyController);
   1126         }
   1127         return isLegacyController;
   1128     }
   1129 
   1130     /**
   1131      * Delete an account.
   1132      */
   1133     public void deleteAccount(final long accountId) {
   1134         EmailAsyncTask.runAsyncParallel(new Runnable() {
   1135             @Override
   1136             public void run() {
   1137                 deleteAccountSync(accountId, mProviderContext);
   1138             }
   1139         });
   1140     }
   1141 
   1142     /**
   1143      * Delete an account synchronously.
   1144      */
   1145     public void deleteAccountSync(long accountId, Context context) {
   1146         try {
   1147             mLegacyControllerMap.remove(accountId);
   1148             // Get the account URI.
   1149             final Account account = Account.restoreAccountWithId(context, accountId);
   1150             if (account == null) {
   1151                 return; // Already deleted?
   1152             }
   1153 
   1154             // Delete account data, attachments, PIM data, etc.
   1155             deleteSyncedDataSync(accountId);
   1156 
   1157             // Now delete the account itself
   1158             Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
   1159             context.getContentResolver().delete(uri, null, null);
   1160 
   1161             // For unit tests, don't run backup, security, and ui pieces.
   1162             if (mInUnitTests) {
   1163                 return;
   1164             }
   1165 
   1166             // Clean up
   1167             AccountBackupRestore.backup(context);
   1168             SecurityPolicy.getInstance(context).reducePolicies();
   1169             Email.setServicesEnabledSync(context);
   1170             Email.setNotifyUiAccountsChanged(true);
   1171             MailService.actionReschedule(context);
   1172         } catch (Exception e) {
   1173             Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
   1174         }
   1175     }
   1176 
   1177     /**
   1178      * Delete all synced data, but don't delete the actual account.  This is used when security
   1179      * policy requirements are not met, and we don't want to reveal any synced data, but we do
   1180      * wish to keep the account configured (e.g. to accept remote wipe commands).
   1181      *
   1182      * The only mailbox not deleted is the account mailbox (if any)
   1183      * Also, clear the sync keys on the remaining account, since the data is gone.
   1184      *
   1185      * SYNCHRONOUS - do not call from UI thread.
   1186      *
   1187      * @param accountId The account to wipe.
   1188      */
   1189     public void deleteSyncedDataSync(long accountId) {
   1190         try {
   1191             // Delete synced attachments
   1192             AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext,
   1193                     accountId);
   1194 
   1195             // Delete synced email, leaving only an empty inbox.  We do this in two phases:
   1196             // 1. Delete all non-inbox mailboxes (which will delete all of their messages)
   1197             // 2. Delete all remaining messages (which will be the inbox messages)
   1198             ContentResolver resolver = mProviderContext.getContentResolver();
   1199             String[] accountIdArgs = new String[] { Long.toString(accountId) };
   1200             resolver.delete(Mailbox.CONTENT_URI,
   1201                     MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION,
   1202                     accountIdArgs);
   1203             resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs);
   1204 
   1205             // Delete sync keys on remaining items
   1206             ContentValues cv = new ContentValues();
   1207             cv.putNull(Account.SYNC_KEY);
   1208             resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs);
   1209             cv.clear();
   1210             cv.putNull(Mailbox.SYNC_KEY);
   1211             resolver.update(Mailbox.CONTENT_URI, cv,
   1212                     MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs);
   1213 
   1214             // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable
   1215             IEmailService service = getServiceForAccount(accountId);
   1216             if (service != null) {
   1217                 service.deleteAccountPIMData(accountId);
   1218             }
   1219         } catch (Exception e) {
   1220             Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e);
   1221         }
   1222     }
   1223 
   1224     /**
   1225      * Simple callback for synchronous commands.  For many commands, this can be largely ignored
   1226      * and the result is observed via provider cursors.  The callback will *not* necessarily be
   1227      * made from the UI thread, so you may need further handlers to safely make UI updates.
   1228      */
   1229     public static abstract class Result {
   1230         private volatile boolean mRegistered;
   1231 
   1232         protected void setRegistered(boolean registered) {
   1233             mRegistered = registered;
   1234         }
   1235 
   1236         protected final boolean isRegistered() {
   1237             return mRegistered;
   1238         }
   1239 
   1240         /**
   1241          * Callback for updateMailboxList
   1242          *
   1243          * @param result If null, the operation completed without error
   1244          * @param accountId The account being operated on
   1245          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
   1246          */
   1247         public void updateMailboxListCallback(MessagingException result, long accountId,
   1248                 int progress) {
   1249         }
   1250 
   1251         /**
   1252          * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
   1253          * it's a separate call used only by UI's, so we can keep things separate.
   1254          *
   1255          * @param result If null, the operation completed without error
   1256          * @param accountId The account being operated on
   1257          * @param mailboxId The mailbox being operated on
   1258          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
   1259          * @param numNewMessages the number of new messages delivered
   1260          */
   1261         public void updateMailboxCallback(MessagingException result, long accountId,
   1262                 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) {
   1263         }
   1264 
   1265         /**
   1266          * Callback for loadMessageForView
   1267          *
   1268          * @param result if null, the attachment completed - if non-null, terminating with failure
   1269          * @param messageId the message which contains the attachment
   1270          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
   1271          */
   1272         public void loadMessageForViewCallback(MessagingException result, long accountId,
   1273                 long messageId, int progress) {
   1274         }
   1275 
   1276         /**
   1277          * Callback for loadAttachment
   1278          *
   1279          * @param result if null, the attachment completed - if non-null, terminating with failure
   1280          * @param messageId the message which contains the attachment
   1281          * @param attachmentId the attachment being loaded
   1282          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
   1283          */
   1284         public void loadAttachmentCallback(MessagingException result, long accountId,
   1285                 long messageId, long attachmentId, int progress) {
   1286         }
   1287 
   1288         /**
   1289          * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
   1290          * it's a separate call used only by the automatic checker service, so we can keep
   1291          * things separate.
   1292          *
   1293          * @param result If null, the operation completed without error
   1294          * @param accountId The account being operated on
   1295          * @param mailboxId The mailbox being operated on (may be unknown at start)
   1296          * @param progress 0 for "starting", no updates, 100 for complete
   1297          * @param tag the same tag that was passed to serviceCheckMail()
   1298          */
   1299         public void serviceCheckMailCallback(MessagingException result, long accountId,
   1300                 long mailboxId, int progress, long tag) {
   1301         }
   1302 
   1303         /**
   1304          * Callback for sending pending messages.  This will be called once to start the
   1305          * group, multiple times for messages, and once to complete the group.
   1306          *
   1307          * Unfortunately this callback works differently on SMTP and EAS.
   1308          *
   1309          * On SMTP:
   1310          *
   1311          * First, we get this.
   1312          *  result == null, messageId == -1, progress == 0:     start batch send
   1313          *
   1314          * Then we get these callbacks per message.
   1315          * (Exchange backend may skip "start sending one message".)
   1316          *  result == null, messageId == xx, progress == 0:     start sending one message
   1317          *  result == xxxx, messageId == xx, progress == 0;     failed sending one message
   1318          *
   1319          * Finally we get this.
   1320          *  result == null, messageId == -1, progres == 100;    finish sending batch
   1321          *
   1322          * On EAS: Almost same as above, except:
   1323          *
   1324          * - There's no first ("start batch send") callback.
   1325          * - accountId is always -1.
   1326          *
   1327          * @param result If null, the operation completed without error
   1328          * @param accountId The account being operated on
   1329          * @param messageId The being sent (may be unknown at start)
   1330          * @param progress 0 for "starting", 100 for complete
   1331          */
   1332         public void sendMailCallback(MessagingException result, long accountId,
   1333                 long messageId, int progress) {
   1334         }
   1335     }
   1336 
   1337     /**
   1338      * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
   1339      * pass down to {@link Result}.
   1340      */
   1341     public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
   1342         private final long mMessageId;
   1343         private final long mAttachmentId;
   1344         private final long mAccountId;
   1345 
   1346         public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
   1347             mMessageId = messageId;
   1348             mAttachmentId = attachmentId;
   1349             mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId);
   1350         }
   1351 
   1352         @Override
   1353         public void loadAttachmentProgress(int progress) {
   1354               synchronized (mListeners) {
   1355                   for (Result listener : mListeners) {
   1356                       listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId,
   1357                               progress);
   1358                  }
   1359               }
   1360         }
   1361 
   1362         @Override
   1363         public void messageRetrieved(com.android.emailcommon.mail.Message message) {
   1364         }
   1365     }
   1366 
   1367     /**
   1368      * Support for receiving callbacks from MessagingController and dealing with UI going
   1369      * out of scope.
   1370      */
   1371     public class LegacyListener extends MessagingListener {
   1372         public LegacyListener() {
   1373         }
   1374 
   1375         @Override
   1376         public void listFoldersStarted(long accountId) {
   1377             synchronized (mListeners) {
   1378                 for (Result l : mListeners) {
   1379                     l.updateMailboxListCallback(null, accountId, 0);
   1380                 }
   1381             }
   1382         }
   1383 
   1384         @Override
   1385         public void listFoldersFailed(long accountId, String message) {
   1386             synchronized (mListeners) {
   1387                 for (Result l : mListeners) {
   1388                     l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
   1389                 }
   1390             }
   1391         }
   1392 
   1393         @Override
   1394         public void listFoldersFinished(long accountId) {
   1395             synchronized (mListeners) {
   1396                 for (Result l : mListeners) {
   1397                     l.updateMailboxListCallback(null, accountId, 100);
   1398                 }
   1399             }
   1400         }
   1401 
   1402         @Override
   1403         public void synchronizeMailboxStarted(long accountId, long mailboxId) {
   1404             synchronized (mListeners) {
   1405                 for (Result l : mListeners) {
   1406                     l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null);
   1407                 }
   1408             }
   1409         }
   1410 
   1411         @Override
   1412         public void synchronizeMailboxFinished(long accountId, long mailboxId,
   1413                 int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
   1414             synchronized (mListeners) {
   1415                 for (Result l : mListeners) {
   1416                     l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages,
   1417                             addedMessages);
   1418                 }
   1419             }
   1420         }
   1421 
   1422         @Override
   1423         public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
   1424             MessagingException me;
   1425             if (e instanceof MessagingException) {
   1426                 me = (MessagingException) e;
   1427             } else {
   1428                 me = new MessagingException(e.toString());
   1429             }
   1430             synchronized (mListeners) {
   1431                 for (Result l : mListeners) {
   1432                     l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null);
   1433                 }
   1434             }
   1435         }
   1436 
   1437         @Override
   1438         public void checkMailStarted(Context context, long accountId, long tag) {
   1439             synchronized (mListeners) {
   1440                 for (Result l : mListeners) {
   1441                     l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
   1442                 }
   1443             }
   1444         }
   1445 
   1446         @Override
   1447         public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
   1448             synchronized (mListeners) {
   1449                 for (Result l : mListeners) {
   1450                     l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
   1451                 }
   1452             }
   1453         }
   1454 
   1455         @Override
   1456         public void loadMessageForViewStarted(long messageId) {
   1457             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
   1458             synchronized (mListeners) {
   1459                 for (Result listener : mListeners) {
   1460                     listener.loadMessageForViewCallback(null, accountId, messageId, 0);
   1461                 }
   1462             }
   1463         }
   1464 
   1465         @Override
   1466         public void loadMessageForViewFinished(long messageId) {
   1467             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
   1468             synchronized (mListeners) {
   1469                 for (Result listener : mListeners) {
   1470                     listener.loadMessageForViewCallback(null, accountId, messageId, 100);
   1471                 }
   1472             }
   1473         }
   1474 
   1475         @Override
   1476         public void loadMessageForViewFailed(long messageId, String message) {
   1477             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
   1478             synchronized (mListeners) {
   1479                 for (Result listener : mListeners) {
   1480                     listener.loadMessageForViewCallback(new MessagingException(message),
   1481                             accountId, messageId, 0);
   1482                 }
   1483             }
   1484         }
   1485 
   1486         @Override
   1487         public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
   1488                 boolean requiresDownload) {
   1489             try {
   1490                 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
   1491                         EmailServiceStatus.IN_PROGRESS, 0);
   1492             } catch (RemoteException e) {
   1493             }
   1494             synchronized (mListeners) {
   1495                 for (Result listener : mListeners) {
   1496                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
   1497                 }
   1498             }
   1499         }
   1500 
   1501         @Override
   1502         public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
   1503             try {
   1504                 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
   1505                         EmailServiceStatus.SUCCESS, 100);
   1506             } catch (RemoteException e) {
   1507             }
   1508             synchronized (mListeners) {
   1509                 for (Result listener : mListeners) {
   1510                     listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
   1511                 }
   1512             }
   1513         }
   1514 
   1515         @Override
   1516         public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
   1517                 MessagingException me, boolean background) {
   1518             try {
   1519                 // If the cause of the MessagingException is an IOException, we send a status of
   1520                 // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to
   1521                 // download the attachment.  Otherwise, the error is considered non-recoverable.
   1522                 int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND;
   1523                 if (me != null && me.getCause() instanceof IOException) {
   1524                     status = EmailServiceStatus.CONNECTION_ERROR;
   1525                 }
   1526                 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0);
   1527             } catch (RemoteException e) {
   1528             }
   1529             synchronized (mListeners) {
   1530                 for (Result listener : mListeners) {
   1531                     // TODO We are overloading the exception here. The UI listens for this
   1532                     // callback and displays a toast if the exception is not null. Since we
   1533                     // want to avoid displaying toast for background operations, we force
   1534                     // the exception to be null. This needs to be re-worked so the UI will
   1535                     // only receive (or at least pays attention to) responses for requests
   1536                     // it explicitly cares about. Then we would not need to overload the
   1537                     // exception parameter.
   1538                     listener.loadAttachmentCallback(background ? null : me, accountId, messageId,
   1539                             attachmentId, 0);
   1540                 }
   1541             }
   1542         }
   1543 
   1544         @Override
   1545         synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
   1546             synchronized (mListeners) {
   1547                 for (Result listener : mListeners) {
   1548                     listener.sendMailCallback(null, accountId, messageId, 0);
   1549                 }
   1550             }
   1551         }
   1552 
   1553         @Override
   1554         synchronized public void sendPendingMessagesCompleted(long accountId) {
   1555             synchronized (mListeners) {
   1556                 for (Result listener : mListeners) {
   1557                     listener.sendMailCallback(null, accountId, -1, 100);
   1558                 }
   1559             }
   1560         }
   1561 
   1562         @Override
   1563         synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
   1564                 Exception reason) {
   1565             MessagingException me;
   1566             if (reason instanceof MessagingException) {
   1567                 me = (MessagingException) reason;
   1568             } else {
   1569                 me = new MessagingException(reason.toString());
   1570             }
   1571             synchronized (mListeners) {
   1572                 for (Result listener : mListeners) {
   1573                     listener.sendMailCallback(me, accountId, messageId, 0);
   1574                 }
   1575             }
   1576         }
   1577     }
   1578 
   1579     /**
   1580      * Service callback for service operations
   1581      */
   1582     private class ServiceCallback extends IEmailServiceCallback.Stub {
   1583 
   1584         private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
   1585 
   1586         public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
   1587                 int progress) {
   1588             MessagingException result = mapStatusToException(statusCode);
   1589             switch (statusCode) {
   1590                 case EmailServiceStatus.SUCCESS:
   1591                     progress = 100;
   1592                     break;
   1593                 case EmailServiceStatus.IN_PROGRESS:
   1594                     if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
   1595                         result = new MessagingException(
   1596                                 String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
   1597                     }
   1598                     // discard progress reports that look like sentinels
   1599                     if (progress < 0 || progress >= 100) {
   1600                         return;
   1601                     }
   1602                     break;
   1603             }
   1604             final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
   1605             synchronized (mListeners) {
   1606                 for (Result listener : mListeners) {
   1607                     listener.loadAttachmentCallback(result, accountId, messageId, attachmentId,
   1608                             progress);
   1609                 }
   1610             }
   1611         }
   1612 
   1613         /**
   1614          * Note, this is an incomplete implementation of this callback, because we are
   1615          * not getting things back from Service in quite the same way as from MessagingController.
   1616          * However, this is sufficient for basic "progress=100" notification that message send
   1617          * has just completed.
   1618          */
   1619         public void sendMessageStatus(long messageId, String subject, int statusCode,
   1620                 int progress) {
   1621             long accountId = -1;        // This should be in the callback
   1622             MessagingException result = mapStatusToException(statusCode);
   1623             switch (statusCode) {
   1624                 case EmailServiceStatus.SUCCESS:
   1625                     progress = 100;
   1626                     break;
   1627                 case EmailServiceStatus.IN_PROGRESS:
   1628                     // discard progress reports that look like sentinels
   1629                     if (progress < 0 || progress >= 100) {
   1630                         return;
   1631                     }
   1632                     break;
   1633             }
   1634             synchronized(mListeners) {
   1635                 for (Result listener : mListeners) {
   1636                     listener.sendMailCallback(result, accountId, messageId, progress);
   1637                 }
   1638             }
   1639         }
   1640 
   1641         public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
   1642             MessagingException result = mapStatusToException(statusCode);
   1643             switch (statusCode) {
   1644                 case EmailServiceStatus.SUCCESS:
   1645                     progress = 100;
   1646                     break;
   1647                 case EmailServiceStatus.IN_PROGRESS:
   1648                     // discard progress reports that look like sentinels
   1649                     if (progress < 0 || progress >= 100) {
   1650                         return;
   1651                     }
   1652                     break;
   1653             }
   1654             synchronized(mListeners) {
   1655                 for (Result listener : mListeners) {
   1656                     listener.updateMailboxListCallback(result, accountId, progress);
   1657                 }
   1658             }
   1659         }
   1660 
   1661         public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
   1662             MessagingException result = mapStatusToException(statusCode);
   1663             switch (statusCode) {
   1664                 case EmailServiceStatus.SUCCESS:
   1665                     progress = 100;
   1666                     break;
   1667                 case EmailServiceStatus.IN_PROGRESS:
   1668                     // discard progress reports that look like sentinels
   1669                     if (progress < 0 || progress >= 100) {
   1670                         return;
   1671                     }
   1672                     break;
   1673             }
   1674             // TODO should pass this back instead of looking it up here
   1675             Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
   1676             // The mailbox could have disappeared if the server commanded it
   1677             if (mbx == null) return;
   1678             long accountId = mbx.mAccountKey;
   1679             synchronized(mListeners) {
   1680                 for (Result listener : mListeners) {
   1681                     listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null);
   1682                 }
   1683             }
   1684         }
   1685 
   1686         private MessagingException mapStatusToException(int statusCode) {
   1687             switch (statusCode) {
   1688                 case EmailServiceStatus.SUCCESS:
   1689                 case EmailServiceStatus.IN_PROGRESS:
   1690                 // Don't generate error if the account is uninitialized
   1691                 case EmailServiceStatus.ACCOUNT_UNINITIALIZED:
   1692                     return null;
   1693 
   1694                 case EmailServiceStatus.LOGIN_FAILED:
   1695                     return new AuthenticationFailedException("");
   1696 
   1697                 case EmailServiceStatus.CONNECTION_ERROR:
   1698                     return new MessagingException(MessagingException.IOERROR);
   1699 
   1700                 case EmailServiceStatus.SECURITY_FAILURE:
   1701                     return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
   1702 
   1703                 case EmailServiceStatus.ACCESS_DENIED:
   1704                     return new MessagingException(MessagingException.ACCESS_DENIED);
   1705 
   1706                 case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
   1707                     return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND);
   1708 
   1709                 case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR:
   1710                     return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR);
   1711 
   1712                 case EmailServiceStatus.MESSAGE_NOT_FOUND:
   1713                 case EmailServiceStatus.FOLDER_NOT_DELETED:
   1714                 case EmailServiceStatus.FOLDER_NOT_RENAMED:
   1715                 case EmailServiceStatus.FOLDER_NOT_CREATED:
   1716                 case EmailServiceStatus.REMOTE_EXCEPTION:
   1717                     // TODO: define exception code(s) & UI string(s) for server-side errors
   1718                 default:
   1719                     return new MessagingException(String.valueOf(statusCode));
   1720             }
   1721         }
   1722 
   1723         @Override
   1724         public void loadMessageStatus(long messageId, int statusCode, int progress)
   1725                 throws RemoteException {
   1726         }
   1727     }
   1728 
   1729     private interface ServiceCallbackWrapper {
   1730         public void call(IEmailServiceCallback cb) throws RemoteException;
   1731     }
   1732 
   1733     /**
   1734      * Proxy that can be used to broadcast service callbacks; we currently use this only for
   1735      * loadAttachment callbacks
   1736      */
   1737     private final IEmailServiceCallback.Stub mCallbackProxy =
   1738         new IEmailServiceCallback.Stub() {
   1739 
   1740         /**
   1741          * Broadcast a callback to the everyone that's registered
   1742          *
   1743          * @param wrapper the ServiceCallbackWrapper used in the broadcast
   1744          */
   1745         private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
   1746             if (sCallbackList != null) {
   1747                 // Call everyone on our callback list
   1748                 // Exceptions can be safely ignored
   1749                 int count = sCallbackList.beginBroadcast();
   1750                 for (int i = 0; i < count; i++) {
   1751                     try {
   1752                         wrapper.call(sCallbackList.getBroadcastItem(i));
   1753                     } catch (RemoteException e) {
   1754                     }
   1755                 }
   1756                 sCallbackList.finishBroadcast();
   1757             }
   1758         }
   1759 
   1760         public void loadAttachmentStatus(final long messageId, final long attachmentId,
   1761                 final int status, final int progress) {
   1762             broadcastCallback(new ServiceCallbackWrapper() {
   1763                 @Override
   1764                 public void call(IEmailServiceCallback cb) throws RemoteException {
   1765                     cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
   1766                 }
   1767             });
   1768         }
   1769 
   1770         @Override
   1771         public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){
   1772         }
   1773 
   1774         @Override
   1775         public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
   1776         }
   1777 
   1778         @Override
   1779         public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
   1780         }
   1781 
   1782         @Override
   1783         public void loadMessageStatus(long messageId, int statusCode, int progress)
   1784                 throws RemoteException {
   1785         }
   1786     };
   1787 
   1788     public static class ControllerService extends Service {
   1789         /**
   1790          * Create our EmailService implementation here.  For now, only loadAttachment is supported;
   1791          * the intention, however, is to move more functionality to the service interface
   1792          */
   1793         private final IEmailService.Stub mBinder = new IEmailService.Stub() {
   1794 
   1795             public Bundle validate(HostAuth hostAuth) {
   1796                 return null;
   1797             }
   1798 
   1799             public Bundle autoDiscover(String userName, String password) {
   1800                 return null;
   1801             }
   1802 
   1803             public void startSync(long mailboxId, boolean userRequest) {
   1804             }
   1805 
   1806             public void stopSync(long mailboxId) {
   1807             }
   1808 
   1809             public void loadAttachment(long attachmentId, boolean background)
   1810                     throws RemoteException {
   1811                 Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this,
   1812                         attachmentId);
   1813                 if (att != null) {
   1814                     if (Email.DEBUG) {
   1815                         Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName);
   1816                     }
   1817                     Message msg = Message.restoreMessageWithId(ControllerService.this,
   1818                             att.mMessageKey);
   1819                     if (msg != null) {
   1820                         // If the message is a forward and the attachment needs downloading, we need
   1821                         // to retrieve the message from the source, rather than from the message
   1822                         // itself
   1823                         if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
   1824                             String[] cols = Utility.getRowColumns(ControllerService.this,
   1825                                     Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY,
   1826                                     new String[] {Long.toString(msg.mId)});
   1827                             if (cols != null) {
   1828                                 msg = Message.restoreMessageWithId(ControllerService.this,
   1829                                         Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN]));
   1830                                 if (msg == null) {
   1831                                     // TODO: We can try restoring from the deleted table here...
   1832                                     return;
   1833                                 }
   1834                             }
   1835                         }
   1836                         MessagingController legacyController = sInstance.mLegacyController;
   1837                         LegacyListener legacyListener = sInstance.mLegacyListener;
   1838                         legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey,
   1839                                 attachmentId, legacyListener, background);
   1840                     } else {
   1841                         // Send back the specific error status for this case
   1842                         sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId,
   1843                                 EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
   1844                     }
   1845                 }
   1846             }
   1847 
   1848             public void updateFolderList(long accountId) {
   1849             }
   1850 
   1851             public void hostChanged(long accountId) {
   1852             }
   1853 
   1854             public void setLogging(int flags) {
   1855             }
   1856 
   1857             public void sendMeetingResponse(long messageId, int response) {
   1858             }
   1859 
   1860             public void loadMore(long messageId) {
   1861             }
   1862 
   1863             // The following three methods are not implemented in this version
   1864             public boolean createFolder(long accountId, String name) {
   1865                 return false;
   1866             }
   1867 
   1868             public boolean deleteFolder(long accountId, String name) {
   1869                 return false;
   1870             }
   1871 
   1872             public boolean renameFolder(long accountId, String oldName, String newName) {
   1873                 return false;
   1874             }
   1875 
   1876             public void setCallback(IEmailServiceCallback cb) {
   1877                 sCallbackList.register(cb);
   1878             }
   1879 
   1880             public void deleteAccountPIMData(long accountId) {
   1881             }
   1882 
   1883             public int searchMessages(long accountId, SearchParams searchParams,
   1884                     long destMailboxId) {
   1885                 return 0;
   1886             }
   1887 
   1888             @Override
   1889             public int getApiLevel() {
   1890                 return Api.LEVEL;
   1891             }
   1892 
   1893             @Override
   1894             public void sendMail(long accountId) throws RemoteException {
   1895             }
   1896         };
   1897 
   1898         @Override
   1899         public IBinder onBind(Intent intent) {
   1900             return mBinder;
   1901         }
   1902     }
   1903 }
   1904