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