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 {
    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         } catch (final CommandStatusException e) {
    204             // TODO: This is the wrong error type.
    205             return RESULT_OTHER_FAILURE;
    206         }
    207         return 0;
    208     }
    209 
    210     @Override
    211     protected long getTimeout() {
    212         if (mInitialSync) {
    213             return 120 * DateUtils.SECOND_IN_MILLIS;
    214         }
    215         return super.getTimeout();
    216     }
    217 
    218     /**
    219      * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
    220      * a different format that excludes the punctuation (this is why I'm not putting this in a
    221      * parent class)
    222      */
    223     private static String formatDateTime(final Calendar calendar) {
    224         final StringBuilder sb = new StringBuilder();
    225         //YYYY-MM-DDTHH:MM:SS.MSSZ
    226         sb.append(calendar.get(Calendar.YEAR));
    227         sb.append('-');
    228         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MONTH) + 1));
    229         sb.append('-');
    230         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
    231         sb.append('T');
    232         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
    233         sb.append(':');
    234         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MINUTE)));
    235         sb.append(':');
    236         sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.SECOND)));
    237         sb.append(".000Z");
    238         return sb.toString();
    239     }
    240 
    241     private final void addOneCollectionToRequest(final Serializer s, final int collectionType,
    242             final String mailboxServerId, final String mailboxSyncKey,
    243             final List<MessageStateChange> stateChanges) throws IOException {
    244 
    245         s.start(Tags.SYNC_COLLECTION);
    246         if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
    247             s.data(Tags.SYNC_CLASS, Eas.getFolderClass(collectionType));
    248         }
    249         s.data(Tags.SYNC_SYNC_KEY, mailboxSyncKey);
    250         s.data(Tags.SYNC_COLLECTION_ID, mailboxServerId);
    251         s.data(Tags.SYNC_GET_CHANGES, "0");
    252         s.start(Tags.SYNC_COMMANDS);
    253         for (final MessageStateChange change : stateChanges) {
    254             s.start(Tags.SYNC_CHANGE);
    255             s.data(Tags.SYNC_SERVER_ID, change.getServerId());
    256             s.start(Tags.SYNC_APPLICATION_DATA);
    257             final int newFlagRead = change.getNewFlagRead();
    258             if (newFlagRead != MessageStateChange.VALUE_UNCHANGED) {
    259                 s.data(Tags.EMAIL_READ, Integer.toString(newFlagRead));
    260             }
    261             final int newFlagFavorite = change.getNewFlagFavorite();
    262             if (newFlagFavorite != MessageStateChange.VALUE_UNCHANGED) {
    263                 // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
    264                 // the boolean "favorite" that we think of in Gmail, but it also represents a
    265                 // follow up action, which can include a subject, start and due dates, and even
    266                 // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
    267                 // require that a flag contain a status, a type, and four date fields, two each
    268                 // for start date and end (due) date.
    269                 if (newFlagFavorite != 0) {
    270                     // Status 2 = set flag
    271                     s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
    272                     // "FollowUp" is the standard type
    273                     s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
    274                     final long now = System.currentTimeMillis();
    275                     final Calendar calendar =
    276                             GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
    277                     calendar.setTimeInMillis(now);
    278                     // Flags are required to have a start date and end date (duplicated)
    279                     // First, we'll set the current date/time in GMT as the start time
    280                     String utc = formatDateTime(calendar);
    281                     s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
    282                     // And then we'll use one week from today for completion date
    283                     calendar.setTimeInMillis(now + DateUtils.WEEK_IN_MILLIS);
    284                     utc = formatDateTime(calendar);
    285                     s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
    286                     s.end();
    287                 } else {
    288                     s.tag(Tags.EMAIL_FLAG);
    289                 }
    290             }
    291             s.end().end();  // SYNC_APPLICATION_DATA, SYNC_CHANGE
    292         }
    293         s.end().end();  // SYNC_COMMANDS, SYNC_COLLECTION
    294     }
    295 }
    296