Home | History | Annotate | Download | only in eas
      1 package com.android.exchange.eas;
      2 
      3 import android.content.ContentResolver;
      4 import android.content.ContentValues;
      5 import android.content.Context;
      6 import android.database.Cursor;
      7 import android.os.Bundle;
      8 import android.provider.CalendarContract;
      9 import android.provider.ContactsContract;
     10 
     11 import com.android.emailcommon.provider.Account;
     12 import com.android.emailcommon.provider.EmailContent;
     13 import com.android.emailcommon.provider.EmailContent.Message;
     14 import com.android.emailcommon.provider.Mailbox;
     15 import com.android.emailcommon.service.EmailServiceStatus;
     16 import com.android.emailcommon.utility.Utility;
     17 import com.android.exchange.CommandStatusException;
     18 import com.android.exchange.Eas;
     19 import com.android.exchange.EasResponse;
     20 import com.android.exchange.service.EasService;
     21 import com.android.mail.providers.UIProvider;
     22 import com.android.mail.utils.LogUtils;
     23 
     24 import org.apache.http.HttpEntity;
     25 
     26 import java.io.IOException;
     27 import java.util.Set;
     28 
     29 public class EasFullSyncOperation extends EasOperation {
     30     private final static String TAG = LogUtils.TAG;
     31 
     32     private final static int RESULT_SUCCESS = 0;
     33     public final static int RESULT_SECURITY_HOLD = -100;
     34 
     35     public static final int SEND_FAILED = 1;
     36     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
     37             EmailContent.MessageColumns.MAILBOX_KEY + "=? and (" +
     38                     EmailContent.SyncColumns.SERVER_ID + " is null or " +
     39                     EmailContent.SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
     40     /**
     41      * The content authorities that can be synced for EAS accounts. Initialization must wait until
     42      * after we have a chance to call {@link EmailContent#init} (and, for future content types,
     43      * possibly other initializations) because that's how we can know what the email authority is.
     44      */
     45     private static String[] AUTHORITIES_TO_SYNC;
     46 
     47     static {
     48         // Statically initialize the authorities we'll sync.
     49         AUTHORITIES_TO_SYNC = new String[] {
     50                 EmailContent.AUTHORITY,
     51                 CalendarContract.AUTHORITY,
     52                 ContactsContract.AUTHORITY
     53         };
     54     }
     55 
     56     final Bundle mSyncExtras;
     57     Set<String> mAuthsToSync;
     58 
     59     public EasFullSyncOperation(final Context context, final Account account,
     60                                 final Bundle syncExtras) {
     61         super(context, account);
     62         mSyncExtras = syncExtras;
     63     }
     64 
     65     @Override
     66     protected String getCommand() {
     67         // This is really a container operation, its performOperation() actually just creates and
     68         // performs a bunch of other operations. It doesn't actually do any of its own
     69         // requests.
     70         // TODO: This is kind of ugly, maybe we need a simpler base class for EasOperation that
     71         // does not assume that it will perform a single network operation.
     72         LogUtils.e(TAG, "unexpected call to EasFullSyncOperation.getCommand");
     73         return null;
     74     }
     75 
     76     @Override
     77     protected HttpEntity getRequestEntity() throws IOException {
     78         // This is really a container operation, its performOperation() actually just creates and
     79         // performs a bunch of other operations. It doesn't actually do any of its own
     80         // requests.
     81         LogUtils.e(TAG, "unexpected call to EasFullSyncOperation.getRequestEntity");
     82         return null;
     83     }
     84 
     85     @Override
     86     protected int handleResponse(final EasResponse response)
     87             throws IOException, CommandStatusException {
     88         // This is really a container operation, its performOperation() actually just creates and
     89         // performs a bunch of other operations. It doesn't actually do any of its own
     90         // requests.
     91         LogUtils.e(TAG, "unexpected call to EasFullSyncOperation.handleResponse");
     92         return RESULT_SUCCESS;
     93     }
     94 
     95     @Override
     96     public int performOperation() {
     97         if (!init()) {
     98             LogUtils.i(LOG_TAG, "Failed to initialize %d before sending request for operation %s",
     99                     getAccountId(), getCommand());
    100             return RESULT_INITIALIZATION_FAILURE;
    101         }
    102 
    103         final android.accounts.Account amAccount = new android.accounts.Account(
    104                 mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    105         mAuthsToSync = EasService.getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC);
    106 
    107         // Figure out what we want to sync, based on the extras and our account sync status.
    108         final boolean isInitialSync = EmailContent.isInitialSyncKey(mAccount.mSyncKey);
    109         final long[] mailboxIds = Mailbox.getMailboxIdsFromBundle(mSyncExtras);
    110         final int mailboxType = mSyncExtras.getInt(Mailbox.SYNC_EXTRA_MAILBOX_TYPE,
    111                 Mailbox.TYPE_NONE);
    112 
    113         final boolean isManual = mSyncExtras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
    114         // Push only means this sync request should only refresh the ping (either because
    115         // settings changed, or we need to restart it for some reason).
    116         final boolean pushOnly = Mailbox.isPushOnlyExtras(mSyncExtras);
    117         // Account only means just do a FolderSync.
    118         final boolean accountOnly = Mailbox.isAccountOnlyExtras(mSyncExtras);
    119         final boolean hasCallbackMethod =
    120                 mSyncExtras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD);
    121         // A "full sync" means that we didn't request a more specific type of sync.
    122         // In this case we sync the folder list and all syncable folders.
    123         final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null &&
    124                 mailboxType == Mailbox.TYPE_NONE);
    125         // A FolderSync is necessary for full sync, initial sync, and account only sync.
    126         final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly);
    127 
    128         int result;
    129 
    130         // Now we will use a bunch of other EasOperations to actually do the sync. Note that
    131         // since we have overridden performOperation, this EasOperation does not have the
    132         // normal handling of errors and retrying that is built in. The handling of errors and
    133         // retries is done in each individual operation.
    134 
    135         // Perform a FolderSync if necessary.
    136         // TODO: We permit FolderSync even during security hold, because it's necessary to
    137         // resolve some holds. Ideally we would only do it for the holds that require it.
    138         if (isFolderSync) {
    139             final EasFolderSync folderSync = new EasFolderSync(mContext, mAccount);
    140             result = folderSync.performOperation();
    141             if (isFatal(result)) {
    142                 // This is a failure, abort the sync.
    143                 LogUtils.i(TAG, "Fatal result %d on folderSync", result);
    144                 return result;
    145             }
    146         }
    147 
    148         // Do not permit further syncs if we're on security hold.
    149         if ((mAccount.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    150             LogUtils.d(TAG, "Account is on security hold %d", mAccount.getId());
    151             return RESULT_SECURITY_HOLD;
    152         }
    153 
    154         if (!isInitialSync) {
    155             EasMoveItems moveOp = new EasMoveItems(mContext, mAccount);
    156             result = moveOp.upsyncMovedMessages();
    157             if (isFatal(result)) {
    158                 // This is a failure, abort the sync.
    159                 LogUtils.i(TAG, "Fatal result %d on MoveItems", result);
    160                 return result;
    161             }
    162 
    163             final EasSync upsync = new EasSync(mContext, mAccount);
    164             result = upsync.upsync();
    165             if (isFatal(result)) {
    166                 // This is a failure, abort the sync.
    167                 LogUtils.i(TAG, "Fatal result %d on upsync", result);
    168                 return result;
    169             }
    170         }
    171 
    172         if (mailboxIds != null) {
    173             // Sync the mailbox that was explicitly requested.
    174             for (final long mailboxId : mailboxIds) {
    175                 result = syncMailbox(mailboxId, hasCallbackMethod, isManual);
    176                 if (isFatal(result)) {
    177                     // This is a failure, abort the sync.
    178                     LogUtils.i(TAG, "Fatal result %d on syncMailbox", result);
    179                     return result;
    180                 }
    181             }
    182         } else if (!accountOnly && !pushOnly) {
    183            // We have to sync multiple folders.
    184             final Cursor c;
    185             if (isFullSync) {
    186                 // Full account sync includes all mailboxes that participate in system sync.
    187                 c = Mailbox.getMailboxIdsForSync(mContext.getContentResolver(), mAccount.mId);
    188             } else {
    189                 // Type-filtered sync should only get the mailboxes of a specific type.
    190                 c = Mailbox.getMailboxIdsForSyncByType(mContext.getContentResolver(),
    191                         mAccount.mId, mailboxType);
    192             }
    193             if (c != null) {
    194                 try {
    195                     while (c.moveToNext()) {
    196                         result = syncMailbox(c.getLong(0), hasCallbackMethod, false);
    197                         if (isFatal(result)) {
    198                             // This is a failure, abort the sync.
    199                             LogUtils.i(TAG, "Fatal result %d on syncMailbox", result);
    200                             return result;
    201                         }
    202                     }
    203                 } finally {
    204                     c.close();
    205                 }
    206             }
    207         }
    208 
    209         return RESULT_SUCCESS;
    210     }
    211 
    212     private int syncMailbox(final long folderId, final boolean hasCallbackMethod,
    213                             final boolean isUserSync) {
    214         final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, folderId);
    215         if (mailbox == null) {
    216             LogUtils.d(TAG, "Could not load folder %d", folderId);
    217             return EasSyncBase.RESULT_HARD_DATA_FAILURE;
    218         }
    219 
    220         if (mailbox.mAccountKey != mAccount.mId) {
    221             LogUtils.e(TAG, "Mailbox does not match account: mailbox %s, %s", mAccount.toString(),
    222                     mSyncExtras);
    223             return EasSyncBase.RESULT_HARD_DATA_FAILURE;
    224         }
    225 
    226         if (mAuthsToSync != null && !mAuthsToSync.contains(Mailbox.getAuthority(mailbox.mType))) {
    227             // We are asking for an account sync, but this mailbox type is not configured for
    228             // sync. Do NOT treat this as a sync error for ping backoff purposes.
    229             return EasSyncBase.RESULT_DONE;
    230         }
    231 
    232         if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
    233             // TODO: Because we don't have bidirectional sync working, trying to downsync
    234             // the drafts folder is confusing. b/11158759
    235             // For now, just disable all syncing of DRAFTS type folders.
    236             // Automatic syncing should always be disabled, but we also stop it here to ensure
    237             // that we won't sync even if the user attempts to force a sync from the UI.
    238             // Do NOT treat as a sync error for ping backoff purposes.
    239             LogUtils.d(TAG, "Skipping sync of DRAFTS folder");
    240             return EmailServiceStatus.SUCCESS;
    241         }
    242 
    243         int syncResult = 0;
    244         // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
    245         // treated as background syncs.
    246         if (mailbox.mType == Mailbox.TYPE_OUTBOX || mailbox.isSyncable()) {
    247             final ContentValues cv = new ContentValues(2);
    248             final int syncStatus = isUserSync ?
    249                     EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND;
    250             updateMailbox(mailbox, cv, syncStatus);
    251             try {
    252                 if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
    253                     return syncOutbox(mailbox.mId);
    254                 }
    255                 if (hasCallbackMethod) {
    256                     final int lastSyncResult = UIProvider.createSyncValue(syncStatus,
    257                             UIProvider.LastSyncResult.SUCCESS);
    258                     EmailServiceStatus.syncMailboxStatus(mContext.getContentResolver(), mSyncExtras,
    259                             mailbox.mId, EmailServiceStatus.IN_PROGRESS, 0, lastSyncResult);
    260                 }
    261                 final EasSyncBase operation = new EasSyncBase(mContext, mAccount, mailbox);
    262                 LogUtils.d(TAG, "IEmailService.syncMailbox account %d", mAccount.mId);
    263                 syncResult = operation.performOperation();
    264             } finally {
    265                 updateMailbox(mailbox, cv, EmailContent.SYNC_STATUS_NONE);
    266                 if (hasCallbackMethod) {
    267                     final int uiSyncResult = translateSyncResultToUiResult(syncResult);
    268                     final int lastSyncResult = UIProvider.createSyncValue(syncStatus, uiSyncResult);
    269                     EmailServiceStatus.syncMailboxStatus(mContext.getContentResolver(), mSyncExtras,
    270                             mailbox.mId, EmailServiceStatus.SUCCESS, 0, lastSyncResult);
    271                 }
    272             }
    273         } else {
    274             // This mailbox is not syncable.
    275             LogUtils.d(TAG, "Skipping sync of non syncable folder");
    276         }
    277 
    278         return syncResult;
    279     }
    280 
    281     private int syncOutbox(final long mailboxId) {
    282         LogUtils.d(TAG, "syncOutbox %d", mAccount.mId);
    283         // Because syncing the outbox uses a single EasOperation for every message, we don't
    284         // want to use doOperation(). That would stop and restart the ping between each operation,
    285         // which is wasteful if we have several messages to send.
    286         final Cursor c = mContext.getContentResolver().query(EmailContent.Message.CONTENT_URI,
    287                 EmailContent.Message.CONTENT_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
    288                 new String[] {Long.toString(mailboxId)}, null);
    289         try {
    290             // Loop through the messages, sending each one
    291             while (c.moveToNext()) {
    292                 final Message message = new Message();
    293                 message.restore(c);
    294                 if (Utility.hasUnloadedAttachments(mContext, message.mId)) {
    295                     // We'll just have to wait on this...
    296                     // TODO: We should make sure that this attachment is queued for download here.
    297                     continue;
    298                 }
    299 
    300                 // TODO: Fix -- how do we want to signal to UI that we started syncing?
    301                 // Note the entire callback mechanism here needs improving.
    302                 //sendMessageStatus(message.mId, null, EmailServiceStatus.IN_PROGRESS, 0);
    303 
    304                 EasOperation op = new EasOutboxSync(mContext, mAccount, message, true);
    305 
    306                 int result = op.performOperation();
    307                 if (result == EasOutboxSync.RESULT_ITEM_NOT_FOUND) {
    308                     // This can happen if we are using smartReply, and the message we are referring
    309                     // to has disappeared from the server. Try again with smartReply disabled.
    310                     // This should be a legitimate, but unusual case. Log a warning.
    311                     LogUtils.w(TAG, "WARNING: EasOutboxSync falling back from smartReply");
    312                     op = new EasOutboxSync(mContext, mAccount, message, false);
    313                     result = op.performOperation();
    314                 }
    315                 // If we got some connection error or other fatal error, terminate the sync.
    316                 // If we get some non-fatal error, continue.
    317                 if (result != EasOutboxSync.RESULT_OK &&
    318                         result != EasOutboxSync.RESULT_NON_FATAL_ERROR &&
    319                         result > EasOutboxSync.RESULT_OP_SPECIFIC_ERROR_RESULT) {
    320                     LogUtils.w(TAG, "Aborting outbox sync for error %d", result);
    321                     return result;
    322                 } else if (result <= EasOutboxSync.RESULT_OP_SPECIFIC_ERROR_RESULT) {
    323                     // There are several different conditions that can cause outbox syncing to fail,
    324                     // but they shouldn't prevent us from continuing and trying to downsync
    325                     // other mailboxes.
    326                     LogUtils.i(TAG, "Outbox sync failed with result %d", result);
    327                 }
    328             }
    329         } finally {
    330             // TODO: Some sort of sendMessageStatus() is needed here.
    331             c.close();
    332         }
    333 
    334         return EasOutboxSync.RESULT_OK;
    335     }
    336 
    337 
    338     /**
    339      * Update the mailbox's sync status with the provider and, if we're finished with the sync,
    340      * write the last sync time as well.
    341      * @param mailbox The mailbox whose sync status to update.
    342      * @param cv A {@link ContentValues} object to use for updating the provider.
    343      * @param syncStatus The status for the current sync.
    344      */
    345     private void updateMailbox(final Mailbox mailbox, final ContentValues cv,
    346                                final int syncStatus) {
    347         cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
    348         if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
    349             cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
    350         }
    351         mailbox.update(mContext, cv);
    352     }
    353 }
    354