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         if (attachInfo == null) {
    750             return;
    751         }
    752 
    753         if (saveToFile.exists() && attachInfo.mContentUri != null) {
    754             // The attachment has already been downloaded, so we will just "pretend" to download it
    755             synchronized (mListeners) {
    756                 for (Result listener : mListeners) {
    757                     listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
    758                 }
    759                 for (Result listener : mListeners) {
    760                     listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
    761                 }
    762             }
    763             return;
    764         }
    765 
    766         // Split here for target type (Service or MessagingController)
    767         IEmailService service = getServiceForMessage(messageId);
    768         if (service != null) {
    769             // Service implementation
    770             try {
    771                 service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(),
    772                         AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString());
    773             } catch (RemoteException e) {
    774                 // TODO Change exception handling to be consistent with however this method
    775                 // is implemented for other protocols
    776                 Log.e("onDownloadAttachment", "RemoteException", e);
    777             }
    778         } else {
    779             // MessagingController implementation
    780             new Thread() {
    781                 @Override
    782                 public void run() {
    783                     mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId,
    784                             mLegacyListener);
    785                 }
    786             }.start();
    787         }
    788     }
    789 
    790     /**
    791      * For a given message id, return a service proxy if applicable, or null.
    792      *
    793      * @param messageId the message of interest
    794      * @result service proxy, or null if n/a
    795      */
    796     private IEmailService getServiceForMessage(long messageId) {
    797         // TODO make this more efficient, caching the account, smaller lookup here, etc.
    798         Message message = Message.restoreMessageWithId(mProviderContext, messageId);
    799         if (message == null) {
    800             return null;
    801         }
    802         return getServiceForAccount(message.mAccountKey);
    803     }
    804 
    805     /**
    806      * For a given account id, return a service proxy if applicable, or null.
    807      *
    808      * TODO this should use a cache because we'll be doing this a lot
    809      *
    810      * @param accountId the message of interest
    811      * @result service proxy, or null if n/a
    812      */
    813     private IEmailService getServiceForAccount(long accountId) {
    814         // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc.
    815         Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId);
    816         if (account == null || isMessagingController(account)) {
    817             return null;
    818         } else {
    819             return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback);
    820         }
    821     }
    822 
    823     /**
    824      * Simple helper to determine if legacy MessagingController should be used
    825      *
    826      * TODO this should not require a full account, just an accountId
    827      * TODO this should use a cache because we'll be doing this a lot
    828      */
    829     public boolean isMessagingController(EmailContent.Account account) {
    830         if (account == null) return false;
    831         Store.StoreInfo info =
    832             Store.StoreInfo.getStoreInfo(account.getStoreUri(mProviderContext), mContext);
    833         // This null happens in testing.
    834         if (info == null) {
    835             return false;
    836         }
    837         String scheme = info.mScheme;
    838 
    839         return ("pop3".equals(scheme) || "imap".equals(scheme));
    840     }
    841 
    842     /**
    843      * Simple callback for synchronous commands.  For many commands, this can be largely ignored
    844      * and the result is observed via provider cursors.  The callback will *not* necessarily be
    845      * made from the UI thread, so you may need further handlers to safely make UI updates.
    846      */
    847     public interface Result {
    848         /**
    849          * Callback for updateMailboxList
    850          *
    851          * @param result If null, the operation completed without error
    852          * @param accountId The account being operated on
    853          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
    854          */
    855         public void updateMailboxListCallback(MessagingException result, long accountId,
    856                 int progress);
    857 
    858         /**
    859          * Callback for updateMailbox.  Note:  This looks a lot like checkMailCallback, but
    860          * it's a separate call used only by UI's, so we can keep things separate.
    861          *
    862          * @param result If null, the operation completed without error
    863          * @param accountId The account being operated on
    864          * @param mailboxId The mailbox being operated on
    865          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
    866          * @param numNewMessages the number of new messages delivered
    867          */
    868         public void updateMailboxCallback(MessagingException result, long accountId,
    869                 long mailboxId, int progress, int numNewMessages);
    870 
    871         /**
    872          * Callback for loadMessageForView
    873          *
    874          * @param result if null, the attachment completed - if non-null, terminating with failure
    875          * @param messageId the message which contains the attachment
    876          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
    877          */
    878         public void loadMessageForViewCallback(MessagingException result, long messageId,
    879                 int progress);
    880 
    881         /**
    882          * Callback for loadAttachment
    883          *
    884          * @param result if null, the attachment completed - if non-null, terminating with failure
    885          * @param messageId the message which contains the attachment
    886          * @param attachmentId the attachment being loaded
    887          * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete
    888          */
    889         public void loadAttachmentCallback(MessagingException result, long messageId,
    890                 long attachmentId, int progress);
    891 
    892         /**
    893          * Callback for checkmail.  Note:  This looks a lot like updateMailboxCallback, but
    894          * it's a separate call used only by the automatic checker service, so we can keep
    895          * things separate.
    896          *
    897          * @param result If null, the operation completed without error
    898          * @param accountId The account being operated on
    899          * @param mailboxId The mailbox being operated on (may be unknown at start)
    900          * @param progress 0 for "starting", no updates, 100 for complete
    901          * @param tag the same tag that was passed to serviceCheckMail()
    902          */
    903         public void serviceCheckMailCallback(MessagingException result, long accountId,
    904                 long mailboxId, int progress, long tag);
    905 
    906         /**
    907          * Callback for sending pending messages.  This will be called once to start the
    908          * group, multiple times for messages, and once to complete the group.
    909          *
    910          * @param result If null, the operation completed without error
    911          * @param accountId The account being operated on
    912          * @param messageId The being sent (may be unknown at start)
    913          * @param progress 0 for "starting", 100 for complete
    914          */
    915         public void sendMailCallback(MessagingException result, long accountId,
    916                 long messageId, int progress);
    917     }
    918 
    919     /**
    920      * Support for receiving callbacks from MessagingController and dealing with UI going
    921      * out of scope.
    922      */
    923     private class LegacyListener extends MessagingListener {
    924 
    925         @Override
    926         public void listFoldersStarted(long accountId) {
    927             synchronized (mListeners) {
    928                 for (Result l : mListeners) {
    929                     l.updateMailboxListCallback(null, accountId, 0);
    930                 }
    931             }
    932         }
    933 
    934         @Override
    935         public void listFoldersFailed(long accountId, String message) {
    936             synchronized (mListeners) {
    937                 for (Result l : mListeners) {
    938                     l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
    939                 }
    940             }
    941         }
    942 
    943         @Override
    944         public void listFoldersFinished(long accountId) {
    945             synchronized (mListeners) {
    946                 for (Result l : mListeners) {
    947                     l.updateMailboxListCallback(null, accountId, 100);
    948                 }
    949             }
    950         }
    951 
    952         @Override
    953         public void synchronizeMailboxStarted(long accountId, long mailboxId) {
    954             synchronized (mListeners) {
    955                 for (Result l : mListeners) {
    956                     l.updateMailboxCallback(null, accountId, mailboxId, 0, 0);
    957                 }
    958             }
    959         }
    960 
    961         @Override
    962         public void synchronizeMailboxFinished(long accountId, long mailboxId,
    963                 int totalMessagesInMailbox, int numNewMessages) {
    964             synchronized (mListeners) {
    965                 for (Result l : mListeners) {
    966                     l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages);
    967                 }
    968             }
    969         }
    970 
    971         @Override
    972         public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
    973             MessagingException me;
    974             if (e instanceof MessagingException) {
    975                 me = (MessagingException) e;
    976             } else {
    977                 me = new MessagingException(e.toString());
    978             }
    979             synchronized (mListeners) {
    980                 for (Result l : mListeners) {
    981                     l.updateMailboxCallback(me, accountId, mailboxId, 0, 0);
    982                 }
    983             }
    984         }
    985 
    986         @Override
    987         public void checkMailStarted(Context context, long accountId, long tag) {
    988             synchronized (mListeners) {
    989                 for (Result l : mListeners) {
    990                     l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
    991                 }
    992             }
    993         }
    994 
    995         @Override
    996         public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
    997             synchronized (mListeners) {
    998                 for (Result l : mListeners) {
    999                     l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
   1000                 }
   1001             }
   1002         }
   1003 
   1004         @Override
   1005         public void loadMessageForViewStarted(long messageId) {
   1006             synchronized (mListeners) {
   1007                 for (Result listener : mListeners) {
   1008                     listener.loadMessageForViewCallback(null, messageId, 0);
   1009                 }
   1010             }
   1011         }
   1012 
   1013         @Override
   1014         public void loadMessageForViewFinished(long messageId) {
   1015             synchronized (mListeners) {
   1016                 for (Result listener : mListeners) {
   1017                     listener.loadMessageForViewCallback(null, messageId, 100);
   1018                 }
   1019             }
   1020         }
   1021 
   1022         @Override
   1023         public void loadMessageForViewFailed(long messageId, String message) {
   1024             synchronized (mListeners) {
   1025                 for (Result listener : mListeners) {
   1026                     listener.loadMessageForViewCallback(new MessagingException(message),
   1027                             messageId, 0);
   1028                 }
   1029             }
   1030         }
   1031 
   1032         @Override
   1033         public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
   1034                 boolean requiresDownload) {
   1035             synchronized (mListeners) {
   1036                 for (Result listener : mListeners) {
   1037                     listener.loadAttachmentCallback(null, messageId, attachmentId, 0);
   1038                 }
   1039             }
   1040         }
   1041 
   1042         @Override
   1043         public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
   1044             synchronized (mListeners) {
   1045                 for (Result listener : mListeners) {
   1046                     listener.loadAttachmentCallback(null, messageId, attachmentId, 100);
   1047                 }
   1048             }
   1049         }
   1050 
   1051         @Override
   1052         public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
   1053                 String reason) {
   1054             synchronized (mListeners) {
   1055                 for (Result listener : mListeners) {
   1056                     listener.loadAttachmentCallback(new MessagingException(reason),
   1057                             messageId, attachmentId, 0);
   1058                 }
   1059             }
   1060         }
   1061 
   1062         @Override
   1063         synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
   1064             synchronized (mListeners) {
   1065                 for (Result listener : mListeners) {
   1066                     listener.sendMailCallback(null, accountId, messageId, 0);
   1067                 }
   1068             }
   1069         }
   1070 
   1071         @Override
   1072         synchronized public void sendPendingMessagesCompleted(long accountId) {
   1073             synchronized (mListeners) {
   1074                 for (Result listener : mListeners) {
   1075                     listener.sendMailCallback(null, accountId, -1, 100);
   1076                 }
   1077             }
   1078         }
   1079 
   1080         @Override
   1081         synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
   1082                 Exception reason) {
   1083             MessagingException me;
   1084             if (reason instanceof MessagingException) {
   1085                 me = (MessagingException) reason;
   1086             } else {
   1087                 me = new MessagingException(reason.toString());
   1088             }
   1089             synchronized (mListeners) {
   1090                 for (Result listener : mListeners) {
   1091                     listener.sendMailCallback(me, accountId, messageId, 0);
   1092                 }
   1093             }
   1094         }
   1095     }
   1096 
   1097     /**
   1098      * Service callback for service operations
   1099      */
   1100     private class ServiceCallback extends IEmailServiceCallback.Stub {
   1101 
   1102         private final static boolean DEBUG_FAIL_DOWNLOADS = false;       // do not check in "true"
   1103 
   1104         public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
   1105                 int progress) {
   1106             MessagingException result = mapStatusToException(statusCode);
   1107             switch (statusCode) {
   1108                 case EmailServiceStatus.SUCCESS:
   1109                     progress = 100;
   1110                     break;
   1111                 case EmailServiceStatus.IN_PROGRESS:
   1112                     if (DEBUG_FAIL_DOWNLOADS && progress > 75) {
   1113                         result = new MessagingException(
   1114                                 String.valueOf(EmailServiceStatus.CONNECTION_ERROR));
   1115                     }
   1116                     // discard progress reports that look like sentinels
   1117                     if (progress < 0 || progress >= 100) {
   1118                         return;
   1119                     }
   1120                     break;
   1121             }
   1122             synchronized (mListeners) {
   1123                 for (Result listener : mListeners) {
   1124                     listener.loadAttachmentCallback(result, messageId, attachmentId, progress);
   1125                 }
   1126             }
   1127         }
   1128 
   1129         /**
   1130          * Note, this is an incomplete implementation of this callback, because we are
   1131          * not getting things back from Service in quite the same way as from MessagingController.
   1132          * However, this is sufficient for basic "progress=100" notification that message send
   1133          * has just completed.
   1134          */
   1135         public void sendMessageStatus(long messageId, String subject, int statusCode,
   1136                 int progress) {
   1137 //            Log.d(Email.LOG_TAG, "sendMessageStatus: messageId=" + messageId
   1138 //                    + " statusCode=" + statusCode + " progress=" + progress);
   1139 //            Log.d(Email.LOG_TAG, "sendMessageStatus: subject=" + subject);
   1140             long accountId = -1;        // This should be in the callback
   1141             MessagingException result = mapStatusToException(statusCode);
   1142             switch (statusCode) {
   1143                 case EmailServiceStatus.SUCCESS:
   1144                     progress = 100;
   1145                     break;
   1146                 case EmailServiceStatus.IN_PROGRESS:
   1147                     // discard progress reports that look like sentinels
   1148                     if (progress < 0 || progress >= 100) {
   1149                         return;
   1150                     }
   1151                     break;
   1152             }
   1153 //            Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId
   1154 //                    + " progress=" + progress);
   1155             synchronized(mListeners) {
   1156                 for (Result listener : mListeners) {
   1157                     listener.sendMailCallback(result, accountId, messageId, progress);
   1158                 }
   1159             }
   1160         }
   1161 
   1162         public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
   1163             MessagingException result = mapStatusToException(statusCode);
   1164             switch (statusCode) {
   1165                 case EmailServiceStatus.SUCCESS:
   1166                     progress = 100;
   1167                     break;
   1168                 case EmailServiceStatus.IN_PROGRESS:
   1169                     // discard progress reports that look like sentinels
   1170                     if (progress < 0 || progress >= 100) {
   1171                         return;
   1172                     }
   1173                     break;
   1174             }
   1175             synchronized(mListeners) {
   1176                 for (Result listener : mListeners) {
   1177                     listener.updateMailboxListCallback(result, accountId, progress);
   1178                 }
   1179             }
   1180         }
   1181 
   1182         public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
   1183             MessagingException result = mapStatusToException(statusCode);
   1184             switch (statusCode) {
   1185                 case EmailServiceStatus.SUCCESS:
   1186                     progress = 100;
   1187                     break;
   1188                 case EmailServiceStatus.IN_PROGRESS:
   1189                     // discard progress reports that look like sentinels
   1190                     if (progress < 0 || progress >= 100) {
   1191                         return;
   1192                     }
   1193                     break;
   1194             }
   1195             // TODO where do we get "number of new messages" as well?
   1196             // TODO should pass this back instead of looking it up here
   1197             // TODO smaller projection
   1198             Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
   1199             // The mailbox could have disappeared if the server commanded it
   1200             if (mbx == null) return;
   1201             long accountId = mbx.mAccountKey;
   1202             synchronized(mListeners) {
   1203                 for (Result listener : mListeners) {
   1204                     listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0);
   1205                 }
   1206             }
   1207         }
   1208 
   1209         private MessagingException mapStatusToException(int statusCode) {
   1210             switch (statusCode) {
   1211                 case EmailServiceStatus.SUCCESS:
   1212                 case EmailServiceStatus.IN_PROGRESS:
   1213                     return null;
   1214 
   1215                 case EmailServiceStatus.LOGIN_FAILED:
   1216                     return new AuthenticationFailedException("");
   1217 
   1218                 case EmailServiceStatus.CONNECTION_ERROR:
   1219                     return new MessagingException(MessagingException.IOERROR);
   1220 
   1221                 case EmailServiceStatus.SECURITY_FAILURE:
   1222                     return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
   1223 
   1224                 case EmailServiceStatus.MESSAGE_NOT_FOUND:
   1225                 case EmailServiceStatus.ATTACHMENT_NOT_FOUND:
   1226                 case EmailServiceStatus.FOLDER_NOT_DELETED:
   1227                 case EmailServiceStatus.FOLDER_NOT_RENAMED:
   1228                 case EmailServiceStatus.FOLDER_NOT_CREATED:
   1229                 case EmailServiceStatus.REMOTE_EXCEPTION:
   1230                     // TODO: define exception code(s) & UI string(s) for server-side errors
   1231                 default:
   1232                     return new MessagingException(String.valueOf(statusCode));
   1233             }
   1234         }
   1235     }
   1236 }
   1237