summaryrefslogtreecommitdiff
path: root/src/com/android/exchange/eas/EasFullSyncOperation.java
blob: 871483466d51fa85eae66f46c7d35286e793919f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
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);
    }
}