blob: 871483466d51fa85eae66f46c7d35286e793919f [file] [log] [blame]
package com.android.exchange.eas;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.exchange.service.EasService;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
import org.apache.http.HttpEntity;
import java.io.IOException;
import java.util.Set;
public class EasFullSyncOperation extends EasOperation {
private final static String TAG = LogUtils.TAG;
private final static int RESULT_SUCCESS = 0;
private final static int RESULT_SECURITY_HOLD = -100;
public static final int SEND_FAILED = 1;
public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
EmailContent.MessageColumns.MAILBOX_KEY + "=? and (" +
EmailContent.SyncColumns.SERVER_ID + " is null or " +
EmailContent.SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
/**
* The content authorities that can be synced for EAS accounts. Initialization must wait until
* after we have a chance to call {@link EmailContent#init} (and, for future content types,
* possibly other initializations) because that's how we can know what the email authority is.
*/
private static String[] AUTHORITIES_TO_SYNC;
static {
// Statically initialize the authorities we'll sync.
AUTHORITIES_TO_SYNC = new String[] {
EmailContent.AUTHORITY,
CalendarContract.AUTHORITY,
ContactsContract.AUTHORITY
};
}
final Bundle mSyncExtras;
Set<String> mAuthsToSync;
public EasFullSyncOperation(final Context context, final long accountId,
final Bundle syncExtras) {
super(context, accountId);
mSyncExtras = syncExtras;
}
@Override
protected String getCommand() {
// This is really a container operation, its performOperation() actually just creates and
// performs a bunch of other operations. It doesn't actually do any of its own
// requests.
// TODO: This is kind of ugly, maybe we need a simpler base class for EasOperation that
// does not assume that it will perform a single network operation.
LogUtils.e(TAG, "unexpected call to EasFullSyncOperation.getCommand");
return null;
}
@Override
protected HttpEntity getRequestEntity() throws IOException {
// This is really a container operation, its performOperation() actually just creates and
// performs a bunch of other operations. It doesn't actually do any of its own
// requests.
LogUtils.e(TAG, "unexpected call to EasFullSyncOperation.getRequestEntity");
return null;
}
@Override
protected int handleResponse(final EasResponse response)
throws IOException, CommandStatusException {
// This is really a container operation, its performOperation() actually just creates and
// performs a bunch of other operations. It doesn't actually do any of its own
// requests.
LogUtils.e(TAG, "unexpected call to EasFullSyncOperation.handleResponse");
return RESULT_SUCCESS;
}
@Override
public int performOperation() {
// Make sure the account is loaded if it hasn't already been.
if (!init(false)) {
LogUtils.i(LOG_TAG, "Failed to initialize %d before operation EasFullSyncOperation",
getAccountId());
return RESULT_INITIALIZATION_FAILURE;
}
final android.accounts.Account amAccount = new android.accounts.Account(
mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
mAuthsToSync = EasService.getAuthoritiesToSync(amAccount, AUTHORITIES_TO_SYNC);
// Figure out what we want to sync, based on the extras and our account sync status.
final boolean isInitialSync = EmailContent.isInitialSyncKey(mAccount.mSyncKey);
final long[] mailboxIds = Mailbox.getMailboxIdsFromBundle(mSyncExtras);
final int mailboxType = mSyncExtras.getInt(Mailbox.SYNC_EXTRA_MAILBOX_TYPE,
Mailbox.TYPE_NONE);
final boolean isManual = mSyncExtras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
// Push only means this sync request should only refresh the ping (either because
// settings changed, or we need to restart it for some reason).
final boolean pushOnly = Mailbox.isPushOnlyExtras(mSyncExtras);
// Account only means just do a FolderSync.
final boolean accountOnly = Mailbox.isAccountOnlyExtras(mSyncExtras);
final boolean hasCallbackMethod =
mSyncExtras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD);
// A "full sync" means that we didn't request a more specific type of sync.
// In this case we sync the folder list and all syncable folders.
final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null &&
mailboxType == Mailbox.TYPE_NONE);
// A FolderSync is necessary for full sync, initial sync, and account only sync.
final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly);
int result;
// Now we will use a bunch of other EasOperations to actually do the sync. Note that
// since we have overridden performOperation, this EasOperation does not have the
// normal handling of errors and retrying that is built in. The handling of errors and
// retries is done in each individual operation.
// Perform a FolderSync if necessary.
// TODO: We permit FolderSync even during security hold, because it's necessary to
// resolve some holds. Ideally we would only do it for the holds that require it.
if (isFolderSync) {
final EasFolderSync folderSync = new EasFolderSync(mContext, mAccount);
result = folderSync.performOperation();
if (isFatal(result)) {
// This is a failure, abort the sync.
LogUtils.i(TAG, "Fatal result %d on folderSync", result);
return result;
}
}
// Do not permit further syncs if we're on security hold.
if ((mAccount.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
return RESULT_SECURITY_HOLD;
}
if (!isInitialSync) {
EasMoveItems moveOp = new EasMoveItems(mContext, mAccount);
result = moveOp.upsyncMovedMessages();
if (isFatal(result)) {
// This is a failure, abort the sync.
LogUtils.i(TAG, "Fatal result %d on MoveItems", result);
return result;
}
final EasSync upsync = new EasSync(mContext, mAccount);
result = upsync.upsync();
if (isFatal(result)) {
// This is a failure, abort the sync.
LogUtils.i(TAG, "Fatal result %d on upsync", result);
return result;
}
}
if (mailboxIds != null) {
// Sync the mailbox that was explicitly requested.
for (final long mailboxId : mailboxIds) {
result = syncMailbox(mailboxId, hasCallbackMethod, isManual);
if (isFatal(result)) {
// This is a failure, abort the sync.
LogUtils.i(TAG, "Fatal result %d on syncMailbox", result);
return result;
}
}
} else if (!accountOnly && !pushOnly) {
// We have to sync multiple folders.
final Cursor c;
if (isFullSync) {
// Full account sync includes all mailboxes that participate in system sync.
c = Mailbox.getMailboxIdsForSync(mContext.getContentResolver(), mAccount.mId);
} else {
// Type-filtered sync should only get the mailboxes of a specific type.
c = Mailbox.getMailboxIdsForSyncByType(mContext.getContentResolver(),
mAccount.mId, mailboxType);
}
if (c != null) {
try {
while (c.moveToNext()) {
result = syncMailbox(c.getLong(0), hasCallbackMethod, false);
if (isFatal(result)) {
// This is a failure, abort the sync.
LogUtils.i(TAG, "Fatal result %d on syncMailbox", result);
return result;
}
}
} finally {
c.close();
}
}
}
return RESULT_SUCCESS;
}
private int syncMailbox(final long folderId, final boolean hasCallbackMethod,
final boolean isUserSync) {
final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, folderId);
if (mailbox == null) {
LogUtils.d(TAG, "Could not load folder %d", folderId);
return EasSyncBase.RESULT_HARD_DATA_FAILURE;
}
if (mailbox.mAccountKey != mAccount.mId) {
LogUtils.e(TAG, "Mailbox does not match account: mailbox %s, %s", mAccount.toString(),
mSyncExtras);
return EasSyncBase.RESULT_HARD_DATA_FAILURE;
}
if (mAuthsToSync != null && !mAuthsToSync.contains(Mailbox.getAuthority(mailbox.mType))) {
// We are asking for an account sync, but this mailbox type is not configured for
// sync. Do NOT treat this as a sync error for ping backoff purposes.
return EasSyncBase.RESULT_DONE;
}
if (mailbox.mType == Mailbox.TYPE_DRAFTS) {
// TODO: Because we don't have bidirectional sync working, trying to downsync
// the drafts folder is confusing. b/11158759
// For now, just disable all syncing of DRAFTS type folders.
// Automatic syncing should always be disabled, but we also stop it here to ensure
// that we won't sync even if the user attempts to force a sync from the UI.
// Do NOT treat as a sync error for ping backoff purposes.
LogUtils.d(TAG, "Skipping sync of DRAFTS folder");
return EmailServiceStatus.SUCCESS;
}
int result = 0;
// Non-mailbox syncs are whole account syncs initiated by the AccountManager and are
// treated as background syncs.
if (mailbox.mType == Mailbox.TYPE_OUTBOX || mailbox.isSyncable()) {
final ContentValues cv = new ContentValues(2);
updateMailbox(mailbox, cv, isUserSync ?
EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND);
try {
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
return syncOutbox(mailbox.mId);
}
if (hasCallbackMethod) {
EmailServiceStatus.syncMailboxStatus(mContext.getContentResolver(), mSyncExtras,
mailbox.mId, EmailServiceStatus.IN_PROGRESS, 0,
UIProvider.LastSyncResult.SUCCESS);
}
final EasSyncBase operation = new EasSyncBase(mContext, mAccount, mailbox);
LogUtils.d(TAG, "IEmailService.syncMailbox account %d", mAccount.mId);
result = operation.performOperation();
} finally {
updateMailbox(mailbox, cv, EmailContent.SYNC_STATUS_NONE);
if (hasCallbackMethod) {
EmailServiceStatus.syncMailboxStatus(mContext.getContentResolver(), mSyncExtras,
mailbox.mId, EmailServiceStatus.SUCCESS, 0,
EasOperation.translateSyncResultToUiResult(result));
}
}
} else {
// This mailbox is not syncable.
LogUtils.d(TAG, "Skipping sync of non syncable folder");
}
return result;
}
private int syncOutbox(final long mailboxId) {
LogUtils.d(TAG, "syncOutbox %d", mAccount.mId);
// Because syncing the outbox uses a single EasOperation for every message, we don't
// want to use doOperation(). That would stop and restart the ping between each operation,
// which is wasteful if we have several messages to send.
final Cursor c = mContext.getContentResolver().query(EmailContent.Message.CONTENT_URI,
EmailContent.Message.CONTENT_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
new String[] {Long.toString(mailboxId)}, null);
try {
// Loop through the messages, sending each one
while (c.moveToNext()) {
final Message message = new Message();
message.restore(c);
if (Utility.hasUnloadedAttachments(mContext, message.mId)) {
// We'll just have to wait on this...
// TODO: We should make sure that this attachment is queued for download here.
continue;
}
// TODO: Fix -- how do we want to signal to UI that we started syncing?
// Note the entire callback mechanism here needs improving.
//sendMessageStatus(message.mId, null, EmailServiceStatus.IN_PROGRESS, 0);
EasOperation op = new EasOutboxSync(mContext, mAccount, message, true);
int result = op.performOperation();
if (result == EasOutboxSync.RESULT_ITEM_NOT_FOUND) {
// This can happen if we are using smartReply, and the message we are referring
// to has disappeared from the server. Try again with smartReply disabled.
op = new EasOutboxSync(mContext, mAccount, message, false);
result = op.performOperation();
}
// If we got some connection error or other fatal error, terminate the sync.
// RESULT_NON_FATAL_ERROR
if (result != EasOutboxSync.RESULT_OK &&
result != EasOutboxSync.RESULT_NON_FATAL_ERROR &&
result > EasOutboxSync.RESULT_OP_SPECIFIC_ERROR_RESULT) {
LogUtils.w(TAG, "Aborting outbox sync for error %d", result);
return result;
}
}
} finally {
// TODO: Some sort of sendMessageStatus() is needed here.
c.close();
}
return EasOutboxSync.RESULT_OK;
}
/**
* Update the mailbox's sync status with the provider and, if we're finished with the sync,
* write the last sync time as well.
* @param mailbox The mailbox whose sync status to update.
* @param cv A {@link ContentValues} object to use for updating the provider.
* @param syncStatus The status for the current sync.
*/
private void updateMailbox(final Mailbox mailbox, final ContentValues cv,
final int syncStatus) {
cv.put(Mailbox.UI_SYNC_STATUS, syncStatus);
if (syncStatus == EmailContent.SYNC_STATUS_NONE) {
cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
}
mailbox.update(mContext, cv);
}
}