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