Home | History | Annotate | Download | only in eas
      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 
     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