1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.exchange.eas;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.support.v4.util.LongSparseArray;
24 import android.text.TextUtils;
25 import android.text.format.DateUtils;
26 
27 import com.android.emailcommon.provider.Account;
28 import com.android.emailcommon.provider.EmailContent;
29 import com.android.emailcommon.provider.Mailbox;
30 import com.android.emailcommon.provider.MessageStateChange;
31 import com.android.exchange.CommandStatusException;
32 import com.android.exchange.Eas;
33 import com.android.exchange.EasResponse;
34 import com.android.exchange.adapter.EmailSyncParser;
35 import com.android.exchange.adapter.Parser;
36 import com.android.exchange.adapter.Serializer;
37 import com.android.exchange.adapter.Tags;
38 import com.android.mail.utils.LogUtils;
39 
40 import org.apache.http.HttpEntity;
41 
42 import java.io.IOException;
43 import java.util.Calendar;
44 import java.util.GregorianCalendar;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Map;
48 import java.util.TimeZone;
49 
50 /**
51  * Performs an Exchange Sync operation for one {@link Mailbox}.
52  * TODO: For now, only handles upsync.
53  * TODO: Handle multiple folders in one request. Not sure if parser can handle it yet.
54  */
55 public class EasSync extends EasOperation {
56 
57     /** Result code indicating that the mailbox for an upsync is no longer present. */
58     public final static int RESULT_NO_MAILBOX = 0;
59     public final static int RESULT_OK = 1;
60 
61     // TODO: When we handle downsync, this will become relevant.
62     private boolean mInitialSync;
63 
64     // State for the mailbox we're currently syncing.
65     private long mMailboxId;
66     private String mMailboxServerId;
67     private String mMailboxSyncKey;
68     private List<MessageStateChange> mStateChanges;
69     private Map<String, Integer> mMessageUpdateStatus;
70 
EasSync(final Context context, final Account account)71     public EasSync(final Context context, final Account account) {
72         super(context, account);
73         mInitialSync = false;
74     }
75 
getMessageId(final String serverId)76     private long getMessageId(final String serverId) {
77         // TODO: Improve this.
78         for (final MessageStateChange change : mStateChanges) {
79             if (change.getServerId().equals(serverId)) {
80                 return change.getMessageId();
81             }
82         }
83         return EmailContent.Message.NO_MESSAGE;
84     }
85 
handleMessageUpdateStatus(final Map<String, Integer> messageStatus, final long[][] messageIds, final int[] counts)86     private void handleMessageUpdateStatus(final Map<String, Integer> messageStatus,
87             final long[][] messageIds, final int[] counts) {
88         for (final Map.Entry<String, Integer> entry : messageStatus.entrySet()) {
89             final String serverId = entry.getKey();
90             final int status = entry.getValue();
91             final int index;
92             if (EmailSyncParser.shouldRetry(status)) {
93                 index = 1;
94             } else {
95                 index = 0;
96             }
97             final long messageId = getMessageId(serverId);
98             if (messageId != EmailContent.Message.NO_MESSAGE) {
99                 messageIds[index][counts[index]] = messageId;
100                 ++counts[index];
101             }
102         }
103     }
104 
105     /**
106      * @return Number of messages successfully synced, or a negative response code from
107      *         {@link EasOperation} if we encountered any errors.
108      */
upsync()109     public final int upsync() {
110         final List<MessageStateChange> changes = MessageStateChange.getChanges(mContext,
111                 getAccountId(), getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE);
112         if (changes == null) {
113             return 0;
114         }
115         final LongSparseArray<List<MessageStateChange>> allData =
116                 MessageStateChange.convertToChangesMap(changes);
117         if (allData == null) {
118             return 0;
119         }
120 
121         final long[][] messageIds = new long[2][changes.size()];
122         final int[] counts = new int[2];
123         int result = 0;
124 
125         for (int i = 0; i < allData.size(); ++i) {
126             mMailboxId = allData.keyAt(i);
127             mStateChanges = allData.valueAt(i);
128             boolean retryMailbox = true;
129             // If we've already encountered a fatal error, don't even try to upsync subsequent
130             // mailboxes.
131             if (result >= 0) {
132                 final Cursor mailboxCursor = mContext.getContentResolver().query(
133                         ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
134                         Mailbox.ProjectionSyncData.PROJECTION, null, null, null);
135                 if (mailboxCursor != null) {
136                     try {
137                         if (mailboxCursor.moveToFirst()) {
138                             mMailboxServerId = mailboxCursor.getString(
139                                     Mailbox.ProjectionSyncData.COLUMN_SERVER_ID);
140                             mMailboxSyncKey = mailboxCursor.getString(
141                                     Mailbox.ProjectionSyncData.COLUMN_SYNC_KEY);
142                             if (TextUtils.isEmpty(mMailboxSyncKey) || mMailboxSyncKey.equals("0")) {
143                                 // For some reason we can get here without a valid mailbox sync key
144                                 // b/10797675
145                                 // TODO: figure out why and clean this up
146                                 LogUtils.d(LOG_TAG,
147                                         "Tried to sync mailbox %d with invalid mailbox sync key",
148                                         mMailboxId);
149                             } else {
150                                 result = performOperation();
151                                 if (result >= 0) {
152                                     // Our request gave us back a legitimate answer; this is the
153                                     // only case in which we don't retry this mailbox.
154                                     retryMailbox = false;
155                                     if (result == RESULT_OK) {
156                                         handleMessageUpdateStatus(mMessageUpdateStatus, messageIds,
157                                                 counts);
158                                     } else if (result == RESULT_NO_MAILBOX) {
159                                         // A retry here is pointless -- the message's mailbox (and
160                                         // therefore the message) is gone, so mark as success so
161                                         // that these entries get wiped from the change list.
162                                         for (final MessageStateChange msc : mStateChanges) {
163                                             messageIds[0][counts[0]] = msc.getMessageId();
164                                             ++counts[0];
165                                         }
166                                     } else {
167                                         LogUtils.wtf(LOG_TAG, "Unrecognized result code: %d",
168                                                 result);
169                                     }
170                                 }
171                             }
172                         }
173                     } finally {
174                         mailboxCursor.close();
175                     }
176                 }
177             }
178             if (retryMailbox) {
179                 for (final MessageStateChange msc : mStateChanges) {
180                     messageIds[1][counts[1]] = msc.getMessageId();
181                     ++counts[1];
182                 }
183             }
184         }
185 
186         final ContentResolver cr = mContext.getContentResolver();
187         MessageStateChange.upsyncSuccessful(cr, messageIds[0], counts[0]);
188         MessageStateChange.upsyncRetry(cr, messageIds[1], counts[1]);
189 
190         if (result < 0) {
191             return result;
192         }
193         return counts[0];
194     }
195 
196     @Override
getCommand()197     protected String getCommand() {
198         return "Sync";
199     }
200 
201     @Override
getRequestEntity()202     protected HttpEntity getRequestEntity() throws IOException {
203         final Serializer s = new Serializer();
204         s.start(Tags.SYNC_SYNC);
205         s.start(Tags.SYNC_COLLECTIONS);
206         addOneCollectionToRequest(s, Mailbox.TYPE_MAIL, mMailboxServerId, mMailboxSyncKey,
207                 mStateChanges);
208         s.end().end().done();
209         return makeEntity(s);
210     }
211 
212     @Override
handleResponse(final EasResponse response)213     protected int handleResponse(final EasResponse response)
214             throws IOException, CommandStatusException {
215         final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
216         if (mailbox == null) {
217             return RESULT_NO_MAILBOX;
218         }
219         final EmailSyncParser parser = new EmailSyncParser(mContext, mContext.getContentResolver(),
220                 response.getInputStream(), mailbox, mAccount);
221         try {
222             parser.parse();
223             mMessageUpdateStatus = parser.getMessageStatuses();
224         } catch (final Parser.EmptyStreamException e) {
225             // This indicates a compressed response which was empty, which is OK.
226         }
227         return RESULT_OK;
228     }
229 
230     @Override
getTimeout()231     protected long getTimeout() {
232         if (mInitialSync) {
233             return 120 * DateUtils.SECOND_IN_MILLIS;
234         }
235         return super.getTimeout();
236     }
237 
238     /**
239      * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
240      * a different format that excludes the punctuation (this is why I'm not putting this in a
241      * parent class)
242      */
formatDateTime(final Calendar calendar)243     private static String formatDateTime(final Calendar calendar) {
244         final StringBuilder sb = new StringBuilder();
245         //YYYY-MM-DDTHH:MM:SS.MSSZ
246         sb.append(calendar.get(Calendar.YEAR));
247         sb.append('-');
248         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MONTH) + 1));
249         sb.append('-');
250         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
251         sb.append('T');
252         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
253         sb.append(':');
254         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MINUTE)));
255         sb.append(':');
256         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.SECOND)));
257         sb.append(".000Z");
258         return sb.toString();
259     }
260 
addOneCollectionToRequest(final Serializer s, final int collectionType, final String mailboxServerId, final String mailboxSyncKey, final List<MessageStateChange> stateChanges)261     private void addOneCollectionToRequest(final Serializer s, final int collectionType,
262             final String mailboxServerId, final String mailboxSyncKey,
263             final List<MessageStateChange> stateChanges) throws IOException {
264 
265         s.start(Tags.SYNC_COLLECTION);
266         if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
267             s.data(Tags.SYNC_CLASS, Eas.getFolderClass(collectionType));
268         }
269         s.data(Tags.SYNC_SYNC_KEY, mailboxSyncKey);
270         s.data(Tags.SYNC_COLLECTION_ID, mailboxServerId);
271         if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
272             // Exchange 2003 doesn't understand the concept of setting this flag to false. The
273             // documentation indicates that its presence alone, with no value, requests a two-way
274             // sync.
275             // TODO: handle downsync here so we don't need this at all
276             s.data(Tags.SYNC_GET_CHANGES, "0");
277         }
278         s.start(Tags.SYNC_COMMANDS);
279         for (final MessageStateChange change : stateChanges) {
280             s.start(Tags.SYNC_CHANGE);
281             s.data(Tags.SYNC_SERVER_ID, change.getServerId());
282             s.start(Tags.SYNC_APPLICATION_DATA);
283             final int newFlagRead = change.getNewFlagRead();
284             if (newFlagRead != MessageStateChange.VALUE_UNCHANGED) {
285                 s.data(Tags.EMAIL_READ, Integer.toString(newFlagRead));
286             }
287             final int newFlagFavorite = change.getNewFlagFavorite();
288             if (newFlagFavorite != MessageStateChange.VALUE_UNCHANGED) {
289                 // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
290                 // the boolean "favorite" that we think of in Gmail, but it also represents a
291                 // follow up action, which can include a subject, start and due dates, and even
292                 // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
293                 // require that a flag contain a status, a type, and four date fields, two each
294                 // for start date and end (due) date.
295                 if (newFlagFavorite != 0) {
296                     // Status 2 = set flag
297                     s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
298                     // "FollowUp" is the standard type
299                     s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
300                     final long now = System.currentTimeMillis();
301                     final Calendar calendar =
302                             GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
303                     calendar.setTimeInMillis(now);
304                     // Flags are required to have a start date and end date (duplicated)
305                     // First, we'll set the current date/time in GMT as the start time
306                     String utc = formatDateTime(calendar);
307                     s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
308                     // And then we'll use one week from today for completion date
309                     calendar.setTimeInMillis(now + DateUtils.WEEK_IN_MILLIS);
310                     utc = formatDateTime(calendar);
311                     s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
312                     s.end();
313                 } else {
314                     s.tag(Tags.EMAIL_FLAG);
315                 }
316             }
317             s.end().end();  // SYNC_APPLICATION_DATA, SYNC_CHANGE
318         }
319         s.end().end();  // SYNC_COMMANDS, SYNC_COLLECTION
320     }
321 }
322