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