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 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 */ 77 public abstract class EasSyncHandler extends EasServerConnection { 78 private static final String TAG = Eas.LOG_TAG; 79 80 public static final int MAX_WINDOW_SIZE = 512; 81 82 /** Window sizes for PIM (contact & calendar) sync options. */ 83 public static final int PIM_WINDOW_SIZE_CONTACTS = 10; 84 public static final int PIM_WINDOW_SIZE_CALENDAR = 10; 85 86 // TODO: For each type of failure, provide info about why. 87 protected static final int SYNC_RESULT_FAILED = -1; 88 protected static final int SYNC_RESULT_DONE = 0; 89 protected static final int SYNC_RESULT_MORE_AVAILABLE = 1; 90 91 /** Maximum number of Sync requests we'll send to the Exchange server in one sync attempt. */ 92 private static final int MAX_LOOPING_COUNT = 100; 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 return SYNC_RESULT_FAILED; 312 } 313 return SYNC_RESULT_DONE; 314 } 315 316 /** 317 * Send one Sync POST to the Exchange server, and handle the response. 318 * @return One of {@link #SYNC_RESULT_FAILED}, {@link #SYNC_RESULT_MORE_AVAILABLE}, or 319 * {@link #SYNC_RESULT_DONE} as appropriate for the server response. 320 * @param syncResult 321 * @param numWindows 322 */ 323 private int performOneSync(SyncResult syncResult, int numWindows) { 324 final String syncKey = getSyncKey(); 325 if (syncKey == null) { 326 return SYNC_RESULT_FAILED; 327 } 328 final boolean initialSync = syncKey.equals("0"); 329 330 final EasResponse resp; 331 try { 332 final Serializer s = buildEasRequest(syncKey, initialSync, numWindows); 333 final long timeout = initialSync ? 120 * DateUtils.SECOND_IN_MILLIS : COMMAND_TIMEOUT; 334 resp = sendHttpClientPost("Sync", s.toByteArray(), timeout); 335 } catch (final IOException e) { 336 LogUtils.e(TAG, e, "Sync error:"); 337 syncResult.stats.numIoExceptions++; 338 return SYNC_RESULT_FAILED; 339 } 340 341 final int result; 342 try { 343 final int code = resp.getStatus(); 344 if (code == HttpStatus.SC_OK) { 345 // A successful sync can have an empty response -- this indicates no change. 346 // In the case of a compressed stream, resp will be non-empty, but parse() handles 347 // that case. 348 if (!resp.isEmpty()) { 349 result = parse(resp); 350 } else { 351 result = SYNC_RESULT_DONE; 352 } 353 } else { 354 LogUtils.e(TAG, "Sync failed with Status: " + code); 355 if (resp.isProvisionError()) { 356 final EasProvision provision = new EasProvision(mContext, mAccount.mId, this); 357 if (provision.provision(syncResult, mAccount.mId)) { 358 // We handled the provisioning error, so loop. 359 result = SYNC_RESULT_MORE_AVAILABLE; 360 } else { 361 syncResult.stats.numAuthExceptions++; 362 return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_SECURITY; 363 } 364 } else if (resp.isAuthError()) { 365 syncResult.stats.numAuthExceptions++; 366 return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_LOGIN; 367 } else { 368 syncResult.stats.numParseExceptions++; 369 return SYNC_RESULT_FAILED; // TODO: Handle SyncStatus.FAILURE_OTHER; 370 } 371 } 372 } finally { 373 resp.close(); 374 } 375 376 cleanup(result); 377 378 if (initialSync && result != SYNC_RESULT_FAILED) { 379 // TODO: Handle Automatic Lookback 380 } 381 382 return result; 383 } 384 385 /** 386 * Perform the sync, updating {@link #mSyncResult} as appropriate (which was passed in from 387 * the system SyncManager and will be read by it on the way out). 388 * This function can send multiple Sync messages to the Exchange server, up to 389 * {@link #MAX_LOOPING_COUNT}, due to the server replying to a Sync request with MoreAvailable. 390 * In the case of errors, this function should not attempt any retries, but rather should 391 * set {@link #mSyncResult} to reflect the problem and let the system SyncManager handle 392 * any it. 393 * @param syncResult 394 */ 395 public final void performSync(SyncResult syncResult) { 396 // Set up traffic stats bookkeeping. 397 final int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount); 398 TrafficStats.setThreadStatsTag(trafficFlags | getTrafficFlag()); 399 400 // TODO: Properly handle UI status updates. 401 //syncMailboxStatus(EmailServiceStatus.IN_PROGRESS, 0); 402 int result = SYNC_RESULT_MORE_AVAILABLE; 403 int numWindows = 0; 404 String key = getSyncKey(); 405 while (result == SYNC_RESULT_MORE_AVAILABLE) { 406 result = performOneSync(syncResult, numWindows); 407 // TODO: Clear pending request queue. 408 ++numWindows; 409 final String newKey = getSyncKey(); 410 if (result == SYNC_RESULT_MORE_AVAILABLE && key.equals(newKey)) { 411 LogUtils.e(TAG, 412 "Server has more data but we have the same key: %s numWindows: %d", 413 key, numWindows); 414 numWindows++; 415 } else { 416 numWindows = 1; 417 } 418 key = newKey; 419 } 420 } 421 } 422