• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
EasFullSyncOperation(final Context context, final Account account, final Bundle syncExtras)59     public EasFullSyncOperation(final Context context, final Account account,
60                                 final Bundle syncExtras) {
61         super(context, account);
62         mSyncExtras = syncExtras;
63     }
64 
65     @Override
getCommand()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
getRequestEntity()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
handleResponse(final EasResponse response)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
performOperation()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 
syncMailbox(final long folderId, final boolean hasCallbackMethod, final boolean isUserSync)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 
syncOutbox(final long mailboxId)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      */
updateMailbox(final Mailbox mailbox, final ContentValues cv, final int syncStatus)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