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