EasSyncHandler.java revision 110837ebff288a75f9bda067c38e2c46797d99b5
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.service; 18 19import android.content.ContentResolver; 20import android.content.Context; 21import android.content.SyncResult; 22import android.net.TrafficStats; 23import android.os.Bundle; 24import android.text.format.DateUtils; 25 26import com.android.emailcommon.TrafficFlags; 27import com.android.emailcommon.provider.Account; 28import com.android.emailcommon.provider.Mailbox; 29import com.android.exchange.CommandStatusException; 30import com.android.exchange.Eas; 31import com.android.exchange.EasResponse; 32import com.android.exchange.adapter.AbstractSyncParser; 33import com.android.exchange.adapter.Parser; 34import com.android.exchange.adapter.Serializer; 35import com.android.exchange.adapter.Tags; 36import com.android.mail.utils.LogUtils; 37 38import org.apache.http.HttpStatus; 39 40import java.io.IOException; 41import java.io.InputStream; 42 43/** 44 * Base class for syncing a single collection from an Exchange server. A "collection" is a single 45 * mailbox, or contacts for an account, or calendar for an account. (Tasks is part of the protocol 46 * but not implemented.) 47 * A single {@link ContentResolver#requestSync} for a single collection corresponds to a single 48 * object (of the appropriate subclass) being created and {@link #performSync} being called on it. 49 * This in turn will result in one or more Sync POST requests being sent to the Exchange server; 50 * from the client's point of view, these multiple Exchange Sync requests are all part of the same 51 * "sync" (i.e. the fact that there are multiple requests to the server is a detail of the Exchange 52 * protocol). 53 * Different collection types (e.g. mail, contacts, calendar) should subclass this class and 54 * implement the various abstract functions. The majority of how the sync flow is common to all, 55 * aside from a few details and the {@link Parser} used. 56 * Details on how this class (and Exchange Sync) works: 57 * - Overview MSDN link: http://msdn.microsoft.com/en-us/library/ee159766(v=exchg.80).aspx 58 * - Sync MSDN link: http://msdn.microsoft.com/en-us/library/gg675638(v=exchg.80).aspx 59 * - The very first time, the client sends a Sync request with SyncKey = 0 and no other parameters. 60 * This initial Sync request simply gets us a real SyncKey. 61 * TODO: We should add the initial Sync to {@link EasAccountSyncHandler}. 62 * - Non-initial Sync requests can be for one or more collections; this implementation does one at 63 * a time. TODO: allow sync for multiple collections to be aggregated? 64 * - For each collection, we send SyncKey, ServerId, other modifiers, Options, and Commands. The 65 * protocol has a specific order in which these elements must appear in the request. 66 * - {@link #buildEasRequest} forms the XML for the request, using {@link #setInitialSyncOptions}, 67 * {@link #setNonInitialSyncOptions}, and {@link #setUpsyncCommands} to fill in the details 68 * specific for each collection type. 69 * - The Sync response may specify that there's more data available on the server, in which case 70 * we keep sending Sync requests to get that data. 71 * - The ordering constraints and other details may require subclasses to have member variables to 72 * store state between the various calls while performing a single Sync request. These may need 73 * to be reset between Sync requests to the Exchange server. Additionally, there are possibly 74 * other necessary cleanups after parsing a Sync response. These are handled in {@link #cleanup}. 75 */ 76public abstract class EasSyncHandler extends EasServerConnection { 77 private static final String TAG = Eas.LOG_TAG; 78 79 /** Window sizes for PIM (contact & calendar) sync options. */ 80 public static final int PIM_WINDOW_SIZE_CONTACTS = 10; 81 public static final int PIM_WINDOW_SIZE_CALENDAR = 10; 82 83 // TODO: For each type of failure, provide info about why. 84 protected static final int SYNC_RESULT_FAILED = -1; 85 protected static final int SYNC_RESULT_DONE = 0; 86 protected static final int SYNC_RESULT_MORE_AVAILABLE = 1; 87 88 /** Maximum number of Sync requests we'll send to the Exchange server in one sync attempt. */ 89 private static final int MAX_LOOPING_COUNT = 100; 90 91 protected final ContentResolver mContentResolver; 92 protected final Mailbox mMailbox; 93 protected final Bundle mSyncExtras; 94 protected final SyncResult mSyncResult; 95 96 protected EasSyncHandler(final Context context, final ContentResolver contentResolver, 97 final Account account, final Mailbox mailbox, final Bundle syncExtras, 98 final SyncResult syncResult) { 99 super(context, account); 100 mContentResolver = contentResolver; 101 mMailbox = mailbox; 102 mSyncExtras = syncExtras; 103 mSyncResult = syncResult; 104 } 105 106 /** 107 * Create an instance of the appropriate subclass to handle sync for mailbox. 108 * @param context 109 * @param contentResolver 110 * @param accountManagerAccount The {@link android.accounts.Account} for this sync. 111 * @param account The {@link Account} for mailbox. 112 * @param mailbox The {@link Mailbox} to sync. 113 * @param syncExtras The extras for this sync, for consumption by {@link #performSync}. 114 * @param syncResult The output results for this sync, which may be written to by 115 * {@link #performSync}. 116 * @return An appropriate EasSyncHandler for this mailbox, or null if this sync can't be 117 * handled. 118 */ 119 public static EasSyncHandler getEasSyncHandler(final Context context, 120 final ContentResolver contentResolver, 121 final android.accounts.Account accountManagerAccount, 122 final Account account, final Mailbox mailbox, 123 final Bundle syncExtras, final SyncResult syncResult) { 124 if (account != null && mailbox != null) { 125 switch (mailbox.mType) { 126 case Mailbox.TYPE_INBOX: 127 case Mailbox.TYPE_MAIL: 128 case Mailbox.TYPE_DRAFTS: 129 case Mailbox.TYPE_SENT: 130 case Mailbox.TYPE_TRASH: 131 return new EasMailboxSyncHandler(context, contentResolver, account, mailbox, 132 syncExtras, syncResult); 133 case Mailbox.TYPE_CALENDAR: 134 return new EasCalendarSyncHandler(context, contentResolver, 135 accountManagerAccount, account, mailbox, syncExtras, syncResult); 136 case Mailbox.TYPE_CONTACTS: 137 return new EasContactsSyncHandler(context, contentResolver, 138 accountManagerAccount, account, mailbox, syncExtras, syncResult); 139 } 140 } 141 // Unknown mailbox type. 142 return null; 143 } 144 145 // Interface for subclasses to implement: 146 // Subclasses must implement the abstract functions below to provide the information needed by 147 // performSync. 148 149 /** 150 * Get the flag for traffic bookkeeping for this sync type. 151 * @return The appropriate value from {@link TrafficFlags} for this sync. 152 */ 153 protected abstract int getTrafficFlag(); 154 155 /** 156 * Get the sync key for this mailbox. 157 * @return The sync key for the object being synced. "0" means this is the first sync. If 158 * there is an error in getting the sync key, this function returns null. 159 */ 160 protected String getSyncKey() { 161 if (mMailbox == null) { 162 return null; 163 } 164 if (mMailbox.mSyncKey == null) { 165 mMailbox.mSyncKey = "0"; 166 } 167 return mMailbox.mSyncKey; 168 } 169 170 /** 171 * Get the folder class name for this mailbox. 172 * @return The string for this folder class, as defined by the Exchange spec. 173 */ 174 // TODO: refactor this to be the same strings as EasPingSyncHandler#handleOneMailbox. 175 protected abstract String getFolderClassName(); 176 177 /** 178 * Return an {@link AbstractSyncParser} appropriate for this sync type and response. 179 * @param is The {@link InputStream} for the {@link EasResponse} for this sync. 180 * @return The {@link AbstractSyncParser} for this response. 181 * @throws IOException 182 */ 183 protected abstract AbstractSyncParser getParser(final InputStream is) throws IOException; 184 185 /** 186 * Add to the {@link Serializer} for this sync the child elements of a Collection needed for an 187 * initial sync for this collection. 188 * @param s The {@link Serializer} for this sync. 189 * @throws IOException 190 */ 191 protected abstract void setInitialSyncOptions(final Serializer s) throws IOException; 192 193 /** 194 * Add to the {@link Serializer} for this sync the child elements of a Collection needed for a 195 * non-initial sync for this collection, OTHER THAN Commands (which are written by 196 * {@link #setUpsyncCommands}. 197 * @param s The {@link Serializer} for this sync. 198 * @throws IOException 199 */ 200 protected abstract void setNonInitialSyncOptions(final Serializer s) throws IOException; 201 202 /** 203 * Add all Commands to the {@link Serializer} for this Sync request. Strictly speaking, it's 204 * not all Upsync requests since Fetch is also a command, but largely that's what this section 205 * is used for. 206 * @param s The {@link Serializer} for this sync. 207 * @throws IOException 208 */ 209 protected abstract void setUpsyncCommands(final Serializer s) throws IOException; 210 211 /** 212 * Perform any necessary cleanup after processing a Sync response. 213 */ 214 protected abstract void cleanup(final int syncResult); 215 216 // End of abstract functions. 217 218 /** 219 * Shared non-initial sync options for PIM (contacts & calendar) objects. 220 * 221 * @param s The {@link com.android.exchange.adapter.Serializer} for this sync request. 222 * @param filter The lookback to use, or null if no lookback is desired. 223 * @param windowSize 224 * @throws IOException 225 */ 226 protected void setPimSyncOptions(final Serializer s, final String filter, int windowSize) 227 throws IOException { 228 s.tag(Tags.SYNC_DELETES_AS_MOVES); 229 s.tag(Tags.SYNC_GET_CHANGES); 230 s.data(Tags.SYNC_WINDOW_SIZE, String.valueOf(windowSize)); 231 s.start(Tags.SYNC_OPTIONS); 232 // Set the filter (lookback), if provided 233 if (filter != null) { 234 s.data(Tags.SYNC_FILTER_TYPE, filter); 235 } 236 // Set the truncation amount and body type 237 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 238 s.start(Tags.BASE_BODY_PREFERENCE); 239 // Plain text 240 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT); 241 s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE); 242 s.end(); 243 } else { 244 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 245 } 246 s.end(); 247 } 248 249 /** 250 * Create and populate the {@link Serializer} for this Sync POST to the Exchange server. 251 * @param syncKey The sync key to use for this request. 252 * @param initialSync Whether this sync is the first for this object. 253 * @return The {@link Serializer} for to use for this request. 254 * @throws IOException 255 */ 256 private Serializer buildEasRequest(final String syncKey, final boolean initialSync) 257 throws IOException { 258 final String className = getFolderClassName(); 259 LogUtils.i(TAG, "Syncing account %d mailbox %d (class %s) with syncKey %s", mAccount.mId, 260 mMailbox.mId, className, syncKey); 261 262 final Serializer s = new Serializer(); 263 264 s.start(Tags.SYNC_SYNC); 265 s.start(Tags.SYNC_COLLECTIONS); 266 s.start(Tags.SYNC_COLLECTION); 267 // The "Class" element is removed in EAS 12.1 and later versions 268 if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) { 269 s.data(Tags.SYNC_CLASS, className); 270 } 271 s.data(Tags.SYNC_SYNC_KEY, syncKey); 272 s.data(Tags.SYNC_COLLECTION_ID, mMailbox.mServerId); 273 if (initialSync) { 274 setInitialSyncOptions(s); 275 } else { 276 setNonInitialSyncOptions(s); 277 setUpsyncCommands(s); 278 } 279 s.end().end().end().done(); 280 281 return s; 282 } 283 284 /** 285 * Interpret a successful (HTTP code = 200) response from the Exchange server. 286 * @param resp The {@link EasResponse} for the Sync message. 287 * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or 288 * {@link #SYNC_RESULT_DONE} as appropriate for the server response. 289 */ 290 private int parse(final EasResponse resp) { 291 try { 292 final AbstractSyncParser parser = getParser(resp.getInputStream()); 293 final boolean moreAvailable = parser.parse(); 294 if (moreAvailable) { 295 return SYNC_RESULT_MORE_AVAILABLE; 296 } 297 } catch (final Parser.EmptyStreamException e) { 298 // This indicates a compressed response which was empty, which is OK. 299 } catch (final IOException e) { 300 return SYNC_RESULT_FAILED; 301 } catch (final CommandStatusException e) { 302 return SYNC_RESULT_FAILED; 303 } 304 return SYNC_RESULT_DONE; 305 } 306 307 /** 308 * Send one Sync POST to the Exchange server, and handle the response. 309 * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or 310 * {@link #SYNC_RESULT_DONE} as appropriate for the server response. 311 */ 312 private int performOneSync() { 313 final String syncKey = getSyncKey(); 314 if (syncKey == null) { 315 return SYNC_RESULT_FAILED; 316 } 317 final boolean initialSync = syncKey.equals("0"); 318 319 final EasResponse resp; 320 try { 321 final Serializer s = buildEasRequest(syncKey, initialSync); 322 final long timeout = initialSync ? 120 * DateUtils.SECOND_IN_MILLIS : COMMAND_TIMEOUT; 323 resp = sendHttpClientPost("Sync", s.toByteArray(), timeout); 324 } catch (final IOException e) { 325 return SYNC_RESULT_FAILED; 326 } 327 328 final int result; 329 try { 330 final int code = resp.getStatus(); 331 if (code == HttpStatus.SC_OK) { 332 // A successful sync can have an empty response -- this indicates no change. 333 // In the case of a compressed stream, resp will be non-empty, but parse() handles 334 // that case. 335 if (!resp.isEmpty()) { 336 result = parse(resp); 337 } else { 338 result = SYNC_RESULT_DONE; 339 } 340 } else if (resp.isProvisionError()) { 341 return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_SECURITY; 342 } else if (resp.isAuthError()) { 343 return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_LOGIN; 344 } else { 345 return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_OTHER; 346 } 347 } finally { 348 resp.close(); 349 } 350 351 cleanup(result); 352 353 if (initialSync && result != SYNC_RESULT_FAILED) { 354 // TODO: Handle Automatic Lookback 355 } 356 357 return result; 358 } 359 360 /** 361 * Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from 362 * the system SyncManager and will be read by it on the way out). 363 * This function can send multiple Sync messages to the Exchange server, up to 364 * {@link #MAX_LOOPING_COUNT}, due to the server replying to a Sync request with MoreAvailable. 365 * In the case of errors, this function should not attempt any retries, but rather should 366 * set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle 367 * any it. 368 */ 369 public final void performSync() { 370 // Set up traffic stats bookkeeping. 371 final int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount); 372 TrafficStats.setThreadStatsTag(trafficFlags | getTrafficFlag()); 373 374 // TODO: Properly handle UI status updates. 375 //syncMailboxStatus(EmailServiceStatus.IN_PROGRESS, 0); 376 int syncResult = SYNC_RESULT_MORE_AVAILABLE; 377 int loopingCount = 0; 378 String key = getSyncKey(); 379 while (syncResult == SYNC_RESULT_MORE_AVAILABLE && loopingCount < MAX_LOOPING_COUNT) { 380 syncResult = performOneSync(); 381 // TODO: Clear pending request queue. 382 ++loopingCount; 383 final String newKey = getSyncKey(); 384 if (syncResult == SYNC_RESULT_MORE_AVAILABLE && key.equals(newKey)) { 385 LogUtils.wtf(TAG, 386 "Server has more data but we have the same key: %s loopingCount: %d", 387 key, loopingCount); 388 } 389 key = newKey; 390 } 391 if (syncResult == SYNC_RESULT_MORE_AVAILABLE) { 392 // TODO: Signal caller that it probably wants to sync again. 393 } 394 } 395} 396