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