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