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