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