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
17package com.android.exchange.eas;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.database.Cursor;
23import android.support.v4.util.LongSparseArray;
24import android.text.TextUtils;
25import android.text.format.DateUtils;
26
27import com.android.emailcommon.provider.Account;
28import com.android.emailcommon.provider.EmailContent;
29import com.android.emailcommon.provider.Mailbox;
30import com.android.emailcommon.provider.MessageStateChange;
31import com.android.exchange.CommandStatusException;
32import com.android.exchange.Eas;
33import com.android.exchange.EasResponse;
34import com.android.exchange.adapter.EmailSyncParser;
35import com.android.exchange.adapter.Parser;
36import com.android.exchange.adapter.Serializer;
37import com.android.exchange.adapter.Tags;
38import com.android.mail.utils.LogUtils;
39
40import org.apache.http.HttpEntity;
41
42import java.io.IOException;
43import java.util.Calendar;
44import java.util.GregorianCalendar;
45import java.util.List;
46import java.util.Locale;
47import java.util.Map;
48import 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 */
55public 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
71    public EasSync(final Context context, final Account account) {
72        super(context, account);
73        mInitialSync = false;
74    }
75
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
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     */
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
197    protected String getCommand() {
198        return "Sync";
199    }
200
201    @Override
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
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
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     */
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
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