EasSyncService.java revision e6c2456aa6c00ef78c6d1d1621511d7ef8507f83
1/* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange; 19 20import android.content.ContentResolver; 21import android.content.ContentUris; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.Entity; 25import android.database.Cursor; 26import android.net.TrafficStats; 27import android.net.Uri; 28import android.os.Build; 29import android.os.Bundle; 30import android.os.RemoteException; 31import android.provider.CalendarContract.Attendees; 32import android.provider.CalendarContract.Events; 33import android.text.TextUtils; 34import android.util.Base64; 35import android.util.Log; 36import android.util.Xml; 37 38import com.android.emailcommon.TrafficFlags; 39import com.android.emailcommon.mail.Address; 40import com.android.emailcommon.mail.MeetingInfo; 41import com.android.emailcommon.mail.MessagingException; 42import com.android.emailcommon.mail.PackedString; 43import com.android.emailcommon.provider.Account; 44import com.android.emailcommon.provider.EmailContent.AccountColumns; 45import com.android.emailcommon.provider.EmailContent.Message; 46import com.android.emailcommon.provider.EmailContent.MessageColumns; 47import com.android.emailcommon.provider.EmailContent.SyncColumns; 48import com.android.emailcommon.provider.HostAuth; 49import com.android.emailcommon.provider.Mailbox; 50import com.android.emailcommon.provider.Policy; 51import com.android.emailcommon.provider.ProviderUnavailableException; 52import com.android.emailcommon.service.EmailServiceConstants; 53import com.android.emailcommon.service.EmailServiceProxy; 54import com.android.emailcommon.service.EmailServiceStatus; 55import com.android.emailcommon.service.PolicyServiceProxy; 56import com.android.emailsync.AbstractSyncService; 57import com.android.emailsync.PartRequest; 58import com.android.emailsync.Request; 59import com.android.emailcommon.utility.EmailClientConnectionManager; 60import com.android.emailcommon.utility.Utility; 61import com.android.exchange.CommandStatusException.CommandStatus; 62import com.android.exchange.adapter.AbstractSyncAdapter; 63import com.android.exchange.adapter.AccountSyncAdapter; 64import com.android.exchange.adapter.AttachmentLoader; 65import com.android.exchange.adapter.CalendarSyncAdapter; 66import com.android.exchange.adapter.ContactsSyncAdapter; 67import com.android.exchange.adapter.EmailSyncAdapter; 68import com.android.exchange.adapter.FolderSyncParser; 69import com.android.exchange.adapter.GalParser; 70import com.android.exchange.adapter.MeetingResponseParser; 71import com.android.exchange.adapter.MoveItemsParser; 72import com.android.exchange.adapter.Parser.EmptyStreamException; 73import com.android.exchange.adapter.ProvisionParser; 74import com.android.exchange.adapter.Serializer; 75import com.android.exchange.adapter.SettingsParser; 76import com.android.exchange.adapter.Tags; 77import com.android.exchange.provider.GalResult; 78import com.android.exchange.utility.CalendarUtilities; 79import com.google.common.annotations.VisibleForTesting; 80 81import org.apache.http.Header; 82import org.apache.http.HttpEntity; 83import org.apache.http.HttpResponse; 84import org.apache.http.HttpStatus; 85import org.apache.http.client.HttpClient; 86import org.apache.http.client.methods.HttpOptions; 87import org.apache.http.client.methods.HttpPost; 88import org.apache.http.client.methods.HttpRequestBase; 89import org.apache.http.entity.ByteArrayEntity; 90import org.apache.http.entity.StringEntity; 91import org.apache.http.impl.client.DefaultHttpClient; 92import org.apache.http.params.BasicHttpParams; 93import org.apache.http.params.HttpConnectionParams; 94import org.apache.http.params.HttpParams; 95import org.xmlpull.v1.XmlPullParser; 96import org.xmlpull.v1.XmlPullParserException; 97import org.xmlpull.v1.XmlPullParserFactory; 98import org.xmlpull.v1.XmlSerializer; 99 100import java.io.ByteArrayOutputStream; 101import java.io.IOException; 102import java.io.InputStream; 103import java.lang.Thread.State; 104import java.net.URI; 105import java.security.cert.CertificateException; 106 107public class EasSyncService extends AbstractSyncService { 108 // DO NOT CHECK IN SET TO TRUE 109 public static final boolean DEBUG_GAL_SERVICE = false; 110 111 protected static final String PING_COMMAND = "Ping"; 112 // Command timeout is the the time allowed for reading data from an open connection before an 113 // IOException is thrown. After a small added allowance, our watchdog alarm goes off (allowing 114 // us to detect a silently dropped connection). The allowance is defined below. 115 static public final int COMMAND_TIMEOUT = 30*SECONDS; 116 // Connection timeout is the time given to connect to the server before reporting an IOException 117 static private final int CONNECTION_TIMEOUT = 20*SECONDS; 118 // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers 119 static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS; 120 121 static private final String AUTO_DISCOVER_SCHEMA_PREFIX = 122 "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/"; 123 static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml"; 124 static protected final int EAS_REDIRECT_CODE = 451; 125 126 static public final int INTERNAL_SERVER_ERROR_CODE = 500; 127 128 static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML"; 129 static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML"; 130 131 static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT; 132 // The amount of time we allow for a thread to release its post lock after receiving an alert 133 static private final int POST_LOCK_TIMEOUT = 10*SECONDS; 134 135 // The EAS protocol Provision status for "we implement all of the policies" 136 static private final String PROVISION_STATUS_OK = "1"; 137 // The EAS protocol Provision status meaning "we partially implement the policies" 138 static private final String PROVISION_STATUS_PARTIAL = "2"; 139 140 static /*package*/ final String DEVICE_TYPE = "Android"; 141 static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' + 142 Eas.CLIENT_VERSION; 143 144 // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before 145 // forcing it to stop. This number has been determined empirically. 146 static private final int MAX_LOOPING_COUNT = 100; 147 // Reasonable default 148 public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION; 149 public Double mProtocolVersionDouble; 150 protected String mDeviceId = null; 151 @VisibleForTesting 152 String mAuthString = null; 153 @VisibleForTesting 154 String mUserString = null; 155 @VisibleForTesting 156 String mBaseUriString = null; 157 public String mHostAddress; 158 public String mUserName; 159 public String mPassword; 160 161 // The HttpPost in progress 162 private volatile HttpPost mPendingPost = null; 163 // Whether a POST was aborted due to alarm (watchdog alarm) 164 protected boolean mPostAborted = false; 165 // Whether a POST was aborted due to reset 166 protected boolean mPostReset = false; 167 168 // The parameters for the connection must be modified through setConnectionParameters 169 private boolean mSsl = true; 170 private boolean mTrustSsl = false; 171 private String mClientCertAlias = null; 172 private int mPort; 173 174 public ContentResolver mContentResolver; 175 // Whether or not the sync service is valid (usable) 176 public boolean mIsValid = true; 177 178 // Whether the most recent upsync failed (status 7) 179 public boolean mUpsyncFailed = false; 180 181 protected EasSyncService(Context _context, Mailbox _mailbox) { 182 super(_context, _mailbox); 183 mContentResolver = _context.getContentResolver(); 184 if (mAccount == null) { 185 mIsValid = false; 186 return; 187 } 188 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 189 if (ha == null) { 190 mIsValid = false; 191 return; 192 } 193 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 194 mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0; 195 } 196 197 private EasSyncService(String prefix) { 198 super(prefix); 199 } 200 201 public EasSyncService() { 202 this("EAS Validation"); 203 } 204 205 public static EasSyncService getServiceForMailbox(Context context, Mailbox m) { 206 switch(m.mType) { 207 case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX: 208 return new EasAccountService(context, m); 209 case Mailbox.TYPE_OUTBOX: 210 return new EasOutboxService(context, m); 211 default: 212 return new EasSyncService(context, m); 213 } 214 } 215 216 public void resetCalendarSyncKey() { 217 CalendarSyncAdapter adapter = new CalendarSyncAdapter(this); 218 try { 219 adapter.setSyncKey("0", false); 220 } catch (IOException e) { 221 // The provider can't be reached; nothing to be done 222 } 223 } 224 225 /** 226 * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its 227 * socket timeout without having thrown an Exception 228 * 229 * @return true if the POST was successfully stopped; false if we've failed and interrupted 230 * the thread 231 */ 232 @Override 233 public boolean alarm() { 234 HttpPost post; 235 if (mThread == null) return true; 236 String threadName = mThread.getName(); 237 238 // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock 239 // executePostWithTimeout (which executes the HttpPost) also uses this lock 240 synchronized(getSynchronizer()) { 241 // Get a reference to the current post lock 242 post = mPendingPost; 243 if (post != null) { 244 if (Eas.USER_LOG) { 245 URI uri = post.getURI(); 246 if (uri != null) { 247 String query = uri.getQuery(); 248 if (query == null) { 249 query = "POST"; 250 } 251 userLog(threadName, ": Alert, aborting ", query); 252 } else { 253 userLog(threadName, ": Alert, no URI?"); 254 } 255 } 256 // Abort the POST 257 mPostAborted = true; 258 post.abort(); 259 } else { 260 // If there's no POST, we're done 261 userLog("Alert, no pending POST"); 262 return true; 263 } 264 } 265 266 // Wait for the POST to finish 267 try { 268 Thread.sleep(POST_LOCK_TIMEOUT); 269 } catch (InterruptedException e) { 270 } 271 272 State s = mThread.getState(); 273 if (Eas.USER_LOG) { 274 userLog(threadName + ": State = " + s.name()); 275 } 276 277 synchronized (getSynchronizer()) { 278 // If the thread is still hanging around and the same post is pending, let's try to 279 // stop the thread with an interrupt. 280 if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) { 281 mStop = true; 282 mThread.interrupt(); 283 userLog("Interrupting..."); 284 // Let the caller know we had to interrupt the thread 285 return false; 286 } 287 } 288 // Let the caller know that the alarm was handled normally 289 return true; 290 } 291 292 @Override 293 public void reset() { 294 synchronized(getSynchronizer()) { 295 if (mPendingPost != null) { 296 URI uri = mPendingPost.getURI(); 297 if (uri != null) { 298 String query = uri.getQuery(); 299 if (query.startsWith("Cmd=Ping")) { 300 userLog("Reset, aborting Ping"); 301 mPostReset = true; 302 mPendingPost.abort(); 303 } 304 } 305 } 306 } 307 } 308 309 @Override 310 public void stop() { 311 mStop = true; 312 synchronized(getSynchronizer()) { 313 if (mPendingPost != null) { 314 mPendingPost.abort(); 315 } 316 } 317 } 318 319 void setupProtocolVersion(EasSyncService service, Header versionHeader) 320 throws MessagingException { 321 // The string is a comma separated list of EAS versions in ascending order 322 // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1 323 String supportedVersions = versionHeader.getValue(); 324 userLog("Server supports versions: ", supportedVersions); 325 String[] supportedVersionsArray = supportedVersions.split(","); 326 String ourVersion = null; 327 // Find the most recent version we support 328 for (String version: supportedVersionsArray) { 329 if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) || 330 version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) || 331 version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1) || 332 version.equals(Eas.SUPPORTED_PROTOCOL_EX2010) || 333 version.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1)) { 334 ourVersion = version; 335 } 336 } 337 // If we don't support any of the servers supported versions, throw an exception here 338 // This will cause validation to fail 339 if (ourVersion == null) { 340 Log.w(TAG, "No supported EAS versions: " + supportedVersions); 341 throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED); 342 } else { 343 // Debug code for testing EAS 14.0; disables support for EAS 14.1 344 // "adb shell setprop log.tag.Exchange14 VERBOSE" 345 if (ourVersion.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1) && 346 Log.isLoggable("Exchange14", Log.VERBOSE)) { 347 ourVersion = Eas.SUPPORTED_PROTOCOL_EX2010; 348 } 349 service.mProtocolVersion = ourVersion; 350 service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion); 351 Account account = service.mAccount; 352 if (account != null) { 353 account.mProtocolVersion = ourVersion; 354 // Fixup search flags, if they're not set 355 if (service.mProtocolVersionDouble >= 12.0 && 356 (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) { 357 if (account.isSaved()) { 358 ContentValues cv = new ContentValues(); 359 account.mFlags |= 360 Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH; 361 cv.put(AccountColumns.FLAGS, account.mFlags); 362 account.update(service.mContext, cv); 363 } 364 } 365 } 366 } 367 } 368 369 /** 370 * Create an EasSyncService for the specified account 371 * 372 * @param context the caller's context 373 * @param account the account 374 * @return the service, or null if the account is on hold or hasn't been initialized 375 */ 376 public static EasSyncService setupServiceForAccount(Context context, Account account) { 377 // Just return null if we're on security hold 378 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 379 return null; 380 } 381 // If there's no protocol version, we're not initialized 382 String protocolVersion = account.mProtocolVersion; 383 if (protocolVersion == null) { 384 return null; 385 } 386 EasSyncService svc = new EasSyncService("OutOfBand"); 387 HostAuth ha = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv); 388 svc.mProtocolVersion = protocolVersion; 389 svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion); 390 svc.mContext = context; 391 svc.mHostAddress = ha.mAddress; 392 svc.mUserName = ha.mLogin; 393 svc.mPassword = ha.mPassword; 394 try { 395 svc.setConnectionParameters(ha); 396 svc.mDeviceId = ExchangeService.getDeviceId(context); 397 } catch (IOException e) { 398 return null; 399 } catch (CertificateException e) { 400 return null; 401 } 402 svc.mAccount = account; 403 return svc; 404 } 405 406 /** 407 * Get a redirect address and validate against it 408 * @param resp the EasResponse to our POST 409 * @param hostAuth the HostAuth we're using to validate 410 * @return true if we have an updated HostAuth (with redirect address); false otherwise 411 */ 412 protected boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) { 413 Header locHeader = resp.getHeader("X-MS-Location"); 414 if (locHeader != null) { 415 String loc; 416 try { 417 loc = locHeader.getValue(); 418 // Reset our host address and uncache our base uri 419 mHostAddress = Uri.parse(loc).getHost(); 420 mBaseUriString = null; 421 hostAuth.mAddress = mHostAddress; 422 userLog("Redirecting to: " + loc); 423 return true; 424 } catch (RuntimeException e) { 425 // Just don't crash if the Uri is illegal 426 } 427 } 428 return false; 429 } 430 431 private static final int MAX_REDIRECTS = 3; 432 private int mRedirectCount = 0; 433 434 @Override 435 public Bundle validateAccount(HostAuth hostAuth, Context context) { 436 Bundle bundle = new Bundle(); 437 int resultCode = MessagingException.NO_ERROR; 438 try { 439 userLog("Testing EAS: ", hostAuth.mAddress, ", ", hostAuth.mLogin, 440 ", ssl = ", hostAuth.shouldUseSsl() ? "1" : "0"); 441 mContext = context; 442 mHostAddress = hostAuth.mAddress; 443 mUserName = hostAuth.mLogin; 444 mPassword = hostAuth.mPassword; 445 446 setConnectionParameters(hostAuth); 447 mDeviceId = ExchangeService.getDeviceId(context); 448 mAccount = new Account(); 449 mAccount.mEmailAddress = hostAuth.mLogin; 450 EasResponse resp = sendHttpClientOptions(); 451 try { 452 int code = resp.getStatus(); 453 userLog("Validation (OPTIONS) response: " + code); 454 if (code == HttpStatus.SC_OK) { 455 // No exception means successful validation 456 Header commands = resp.getHeader("MS-ASProtocolCommands"); 457 Header versions = resp.getHeader("ms-asprotocolversions"); 458 // Make sure we've got the right protocol version set up 459 try { 460 if (commands == null || versions == null) { 461 userLog("OPTIONS response without commands or versions"); 462 // We'll treat this as a protocol exception 463 throw new MessagingException(0); 464 } 465 setupProtocolVersion(this, versions); 466 } catch (MessagingException e) { 467 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, 468 MessagingException.PROTOCOL_VERSION_UNSUPPORTED); 469 return bundle; 470 } 471 472 // Run second test here for provisioning failures using FolderSync 473 userLog("Try folder sync"); 474 // Send "0" as the sync key for new accounts; otherwise, use the current key 475 String syncKey = "0"; 476 Account existingAccount = Utility.findExistingAccount( 477 context, -1L, hostAuth.mAddress, hostAuth.mLogin); 478 if (existingAccount != null && existingAccount.mSyncKey != null) { 479 syncKey = existingAccount.mSyncKey; 480 } 481 Serializer s = new Serializer(); 482 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey) 483 .end().end().done(); 484 resp = sendHttpClientPost("FolderSync", s.toByteArray()); 485 code = resp.getStatus(); 486 // Handle HTTP error responses accordingly 487 if (code == HttpStatus.SC_FORBIDDEN) { 488 // For validation only, we take 403 as ACCESS_DENIED (the account isn't 489 // authorized, possibly due to device type) 490 resultCode = MessagingException.ACCESS_DENIED; 491 } else if (EasResponse.isProvisionError(code)) { 492 // The device needs to have security policies enforced 493 throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING); 494 } else if (code == HttpStatus.SC_NOT_FOUND) { 495 // We get a 404 from OWA addresses (which are NOT EAS addresses) 496 resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED; 497 } else if (code == HttpStatus.SC_UNAUTHORIZED) { 498 resultCode = resp.isMissingCertificate() 499 ? MessagingException.CLIENT_CERTIFICATE_REQUIRED 500 : MessagingException.AUTHENTICATION_FAILED; 501 } else if (code != HttpStatus.SC_OK) { 502 if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) && 503 getValidateRedirect(resp, hostAuth)) { 504 return validateAccount(hostAuth, context); 505 } 506 // Fail generically with anything other than success 507 userLog("Unexpected response for FolderSync: ", code); 508 resultCode = MessagingException.UNSPECIFIED_EXCEPTION; 509 } else { 510 // We need to parse the result to see if we've got a provisioning issue 511 // (EAS 14.0 only) 512 if (!resp.isEmpty()) { 513 InputStream is = resp.getInputStream(); 514 // Create the parser with statusOnly set to true; we only care about 515 // seeing if a CommandStatusException is thrown (indicating a 516 // provisioning failure) 517 new FolderSyncParser(is, new AccountSyncAdapter(this), true).parse(); 518 } 519 userLog("Validation successful"); 520 } 521 } else if (EasResponse.isAuthError(code)) { 522 userLog("Authentication failed"); 523 resultCode = resp.isMissingCertificate() 524 ? MessagingException.CLIENT_CERTIFICATE_REQUIRED 525 : MessagingException.AUTHENTICATION_FAILED; 526 } else if (code == INTERNAL_SERVER_ERROR_CODE) { 527 // For Exchange 2003, this could mean an authentication failure OR server error 528 userLog("Internal server error"); 529 resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR; 530 } else { 531 if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) && 532 getValidateRedirect(resp, hostAuth)) { 533 return validateAccount(hostAuth, context); 534 } 535 // TODO Need to catch other kinds of errors (e.g. policy) For now, report code. 536 userLog("Validation failed, reporting I/O error: ", code); 537 resultCode = MessagingException.IOERROR; 538 } 539 } catch (CommandStatusException e) { 540 int status = e.mStatus; 541 if (CommandStatus.isNeedsProvisioning(status)) { 542 // Get the policies and see if we are able to support them 543 ProvisionParser pp = canProvision(this); 544 if (pp != null && pp.hasSupportablePolicySet()) { 545 // Set the proper result code and save the PolicySet in our Bundle 546 resultCode = MessagingException.SECURITY_POLICIES_REQUIRED; 547 bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET, 548 pp.getPolicy()); 549 if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 550 mAccount.mSecuritySyncKey = pp.getSecuritySyncKey(); 551 if (!sendSettings()) { 552 userLog("Denied access: ", CommandStatus.toString(status)); 553 resultCode = MessagingException.ACCESS_DENIED; 554 } 555 } 556 } else { 557 // If not, set the proper code (the account will not be created) 558 resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED; 559 bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET, 560 pp.getPolicy()); 561 } 562 } else if (CommandStatus.isDeniedAccess(status)) { 563 userLog("Denied access: ", CommandStatus.toString(status)); 564 resultCode = MessagingException.ACCESS_DENIED; 565 } else if (CommandStatus.isTransientError(status)) { 566 userLog("Transient error: ", CommandStatus.toString(status)); 567 resultCode = MessagingException.IOERROR; 568 } else { 569 userLog("Unexpected response: ", CommandStatus.toString(status)); 570 resultCode = MessagingException.UNSPECIFIED_EXCEPTION; 571 } 572 } finally { 573 resp.close(); 574 } 575 } catch (IOException e) { 576 Throwable cause = e.getCause(); 577 if (cause != null && cause instanceof CertificateException) { 578 // This could be because the server's certificate failed to validate. 579 userLog("CertificateException caught: ", e.getMessage()); 580 resultCode = MessagingException.GENERAL_SECURITY; 581 } 582 userLog("IOException caught: ", e.getMessage()); 583 resultCode = MessagingException.IOERROR; 584 } catch (CertificateException e) { 585 // This occurs if the client certificate the user specified is invalid/inaccessible. 586 userLog("CertificateException caught: ", e.getMessage()); 587 resultCode = MessagingException.CLIENT_CERTIFICATE_ERROR; 588 } 589 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode); 590 return bundle; 591 } 592 593 /** 594 * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that 595 * it can be reused 596 * 597 * @param resp the HttpResponse that indicates a redirect (451) 598 * @param post the HttpPost that was originally sent to the server 599 * @return the HttpPost, updated with the redirect location 600 */ 601 private HttpPost getRedirect(HttpResponse resp, HttpPost post) { 602 Header locHeader = resp.getFirstHeader("X-MS-Location"); 603 if (locHeader != null) { 604 String loc = locHeader.getValue(); 605 // If we've gotten one and it shows signs of looking like an address, we try 606 // sending our request there 607 if (loc != null && loc.startsWith("http")) { 608 post.setURI(URI.create(loc)); 609 return post; 610 } 611 } 612 return null; 613 } 614 615 /** 616 * Send the POST command to the autodiscover server, handling a redirect, if necessary, and 617 * return the HttpResponse. If we get a 401 (unauthorized) error and we're using the 618 * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com) 619 * 620 * @param client the HttpClient to be used for the request 621 * @param post the HttpPost we're going to send 622 * @param canRetry whether we can retry using the bare name on an authentication failure (401) 623 * @return an HttpResponse from the original or redirect server 624 * @throws IOException on any IOException within the HttpClient code 625 * @throws MessagingException 626 */ 627 private EasResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry) 628 throws IOException, MessagingException { 629 userLog("Posting autodiscover to: " + post.getURI()); 630 EasResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT); 631 int code = resp.getStatus(); 632 // On a redirect, try the new location 633 if (code == EAS_REDIRECT_CODE) { 634 post = getRedirect(resp.mResponse, post); 635 if (post != null) { 636 userLog("Posting autodiscover to redirect: " + post.getURI()); 637 return executePostWithTimeout(client, post, COMMAND_TIMEOUT); 638 } 639 // 401 (Unauthorized) is for true auth errors when used in Autodiscover 640 } else if (code == HttpStatus.SC_UNAUTHORIZED) { 641 if (canRetry && mUserName.contains("@")) { 642 // Try again using the bare user name 643 int atSignIndex = mUserName.indexOf('@'); 644 mUserName = mUserName.substring(0, atSignIndex); 645 cacheAuthUserAndBaseUriStrings(); 646 userLog("401 received; trying username: ", mUserName); 647 // Recreate the basic authentication string and reset the header 648 post.removeHeaders("Authorization"); 649 post.setHeader("Authorization", mAuthString); 650 return postAutodiscover(client, post, false); 651 } 652 throw new MessagingException(MessagingException.AUTHENTICATION_FAILED); 653 // 403 (and others) we'll just punt on 654 } else if (code != HttpStatus.SC_OK) { 655 // We'll try the next address if this doesn't work 656 userLog("Code: " + code + ", throwing IOException"); 657 throw new IOException(); 658 } 659 return resp; 660 } 661 662 /** 663 * Convert an EAS server url to a HostAuth host address 664 * @param url a url, as provided by the Exchange server 665 * @return our equivalent host address 666 */ 667 protected String autodiscoverUrlToHostAddress(String url) { 668 if (url == null) return null; 669 // We need to extract the server address from a url 670 return Uri.parse(url).getHost(); 671 } 672 673 /** 674 * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using 675 * only an email address and the password 676 * 677 * @param userName the user's email address 678 * @param password the user's password 679 * @return a HostAuth ready to be saved in an Account or null (failure) 680 */ 681 public Bundle tryAutodiscover(String userName, String password) throws RemoteException { 682 XmlSerializer s = Xml.newSerializer(); 683 ByteArrayOutputStream os = new ByteArrayOutputStream(1024); 684 HostAuth hostAuth = new HostAuth(); 685 Bundle bundle = new Bundle(); 686 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 687 MessagingException.NO_ERROR); 688 try { 689 // Build the XML document that's sent to the autodiscover server(s) 690 s.setOutput(os, "UTF-8"); 691 s.startDocument("UTF-8", false); 692 s.startTag(null, "Autodiscover"); 693 s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006"); 694 s.startTag(null, "Request"); 695 s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress"); 696 s.startTag(null, "AcceptableResponseSchema"); 697 s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006"); 698 s.endTag(null, "AcceptableResponseSchema"); 699 s.endTag(null, "Request"); 700 s.endTag(null, "Autodiscover"); 701 s.endDocument(); 702 String req = os.toString(); 703 704 // Initialize the user name and password 705 mUserName = userName; 706 mPassword = password; 707 // Port is always 443 and SSL is used 708 mPort = 443; 709 mSsl = true; 710 711 // Make sure the authentication string is recreated and cached 712 cacheAuthUserAndBaseUriStrings(); 713 714 // Split out the domain name 715 int amp = userName.indexOf('@'); 716 // The UI ensures that userName is a valid email address 717 if (amp < 0) { 718 throw new RemoteException(); 719 } 720 String domain = userName.substring(amp + 1); 721 722 // There are up to four attempts here; the two URLs that we're supposed to try per the 723 // specification, and up to one redirect for each (handled in postAutodiscover) 724 // Note: The expectation is that, of these four attempts, only a single server will 725 // actually be identified as the autodiscover server. For the identified server, 726 // we may also try a 2nd connection with a different format (bare name). 727 728 // Try the domain first and see if we can get a response 729 HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE); 730 setHeaders(post, false); 731 post.setHeader("Content-Type", "text/xml"); 732 post.setEntity(new StringEntity(req)); 733 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 734 EasResponse resp; 735 try { 736 resp = postAutodiscover(client, post, true /*canRetry*/); 737 } catch (IOException e1) { 738 userLog("IOException in autodiscover; trying alternate address"); 739 // We catch the IOException here because we have an alternate address to try 740 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE)); 741 // If we fail here, we're out of options, so we let the outer try catch the 742 // IOException and return null 743 resp = postAutodiscover(client, post, true /*canRetry*/); 744 } 745 746 try { 747 // Get the "final" code; if it's not 200, just return null 748 int code = resp.getStatus(); 749 userLog("Code: " + code); 750 if (code != HttpStatus.SC_OK) return null; 751 752 InputStream is = resp.getInputStream(); 753 // The response to Autodiscover is regular XML (not WBXML) 754 // If we ever get an error in this process, we'll just punt and return null 755 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 756 XmlPullParser parser = factory.newPullParser(); 757 parser.setInput(is, "UTF-8"); 758 int type = parser.getEventType(); 759 if (type == XmlPullParser.START_DOCUMENT) { 760 type = parser.next(); 761 if (type == XmlPullParser.START_TAG) { 762 String name = parser.getName(); 763 if (name.equals("Autodiscover")) { 764 hostAuth = new HostAuth(); 765 parseAutodiscover(parser, hostAuth); 766 // On success, we'll have a server address and login 767 if (hostAuth.mAddress != null) { 768 // Fill in the rest of the HostAuth 769 // We use the user name and password that were successful during 770 // the autodiscover process 771 hostAuth.mLogin = mUserName; 772 hostAuth.mPassword = mPassword; 773 // Note: there is no way we can auto-discover the proper client 774 // SSL certificate to use, if one is needed. 775 hostAuth.mPort = 443; 776 hostAuth.mProtocol = "eas"; 777 hostAuth.mFlags = 778 HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE; 779 bundle.putParcelable( 780 EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth); 781 } else { 782 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 783 MessagingException.UNSPECIFIED_EXCEPTION); 784 } 785 } 786 } 787 } 788 } catch (XmlPullParserException e1) { 789 // This would indicate an I/O error of some sort 790 // We will simply return null and user can configure manually 791 } finally { 792 resp.close(); 793 } 794 // There's no reason at all for exceptions to be thrown, and it's ok if so. 795 // We just won't do auto-discover; user can configure manually 796 } catch (IllegalArgumentException e) { 797 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 798 MessagingException.UNSPECIFIED_EXCEPTION); 799 } catch (IllegalStateException e) { 800 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 801 MessagingException.UNSPECIFIED_EXCEPTION); 802 } catch (IOException e) { 803 userLog("IOException in Autodiscover", e); 804 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 805 MessagingException.IOERROR); 806 } catch (MessagingException e) { 807 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 808 MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED); 809 } 810 return bundle; 811 } 812 813 void parseServer(XmlPullParser parser, HostAuth hostAuth) 814 throws XmlPullParserException, IOException { 815 boolean mobileSync = false; 816 while (true) { 817 int type = parser.next(); 818 if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) { 819 break; 820 } else if (type == XmlPullParser.START_TAG) { 821 String name = parser.getName(); 822 if (name.equals("Type")) { 823 if (parser.nextText().equals("MobileSync")) { 824 mobileSync = true; 825 } 826 } else if (mobileSync && name.equals("Url")) { 827 String hostAddress = 828 autodiscoverUrlToHostAddress(parser.nextText()); 829 if (hostAddress != null) { 830 hostAuth.mAddress = hostAddress; 831 userLog("Autodiscover, server: " + hostAddress); 832 } 833 } 834 } 835 } 836 } 837 838 void parseSettings(XmlPullParser parser, HostAuth hostAuth) 839 throws XmlPullParserException, IOException { 840 while (true) { 841 int type = parser.next(); 842 if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) { 843 break; 844 } else if (type == XmlPullParser.START_TAG) { 845 String name = parser.getName(); 846 if (name.equals("Server")) { 847 parseServer(parser, hostAuth); 848 } 849 } 850 } 851 } 852 853 void parseAction(XmlPullParser parser, HostAuth hostAuth) 854 throws XmlPullParserException, IOException { 855 while (true) { 856 int type = parser.next(); 857 if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) { 858 break; 859 } else if (type == XmlPullParser.START_TAG) { 860 String name = parser.getName(); 861 if (name.equals("Error")) { 862 // Should parse the error 863 } else if (name.equals("Redirect")) { 864 Log.d(TAG, "Redirect: " + parser.nextText()); 865 } else if (name.equals("Settings")) { 866 parseSettings(parser, hostAuth); 867 } 868 } 869 } 870 } 871 872 void parseUser(XmlPullParser parser, HostAuth hostAuth) 873 throws XmlPullParserException, IOException { 874 while (true) { 875 int type = parser.next(); 876 if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) { 877 break; 878 } else if (type == XmlPullParser.START_TAG) { 879 String name = parser.getName(); 880 if (name.equals("EMailAddress")) { 881 String addr = parser.nextText(); 882 userLog("Autodiscover, email: " + addr); 883 } else if (name.equals("DisplayName")) { 884 String dn = parser.nextText(); 885 userLog("Autodiscover, user: " + dn); 886 } 887 } 888 } 889 } 890 891 void parseResponse(XmlPullParser parser, HostAuth hostAuth) 892 throws XmlPullParserException, IOException { 893 while (true) { 894 int type = parser.next(); 895 if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) { 896 break; 897 } else if (type == XmlPullParser.START_TAG) { 898 String name = parser.getName(); 899 if (name.equals("User")) { 900 parseUser(parser, hostAuth); 901 } else if (name.equals("Action")) { 902 parseAction(parser, hostAuth); 903 } 904 } 905 } 906 } 907 908 void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth) 909 throws XmlPullParserException, IOException { 910 while (true) { 911 int type = parser.nextTag(); 912 if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) { 913 break; 914 } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) { 915 parseResponse(parser, hostAuth); 916 } 917 } 918 } 919 920 /** 921 * Contact the GAL and obtain a list of matching accounts 922 * @param context caller's context 923 * @param accountId the account Id to search 924 * @param filter the characters entered so far 925 * @return a result record or null for no data 926 * 927 * TODO: shorter timeout for interactive lookup 928 * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0) 929 * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion 930 */ 931 static public GalResult searchGal(Context context, long accountId, String filter, int limit) { 932 Account acct = Account.restoreAccountWithId(context, accountId); 933 if (acct != null) { 934 EasSyncService svc = setupServiceForAccount(context, acct); 935 if (svc == null) return null; 936 try { 937 Serializer s = new Serializer(); 938 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE); 939 s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter); 940 s.start(Tags.SEARCH_OPTIONS); 941 s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1)); 942 s.end().end().end().done(); 943 EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray()); 944 try { 945 int code = resp.getStatus(); 946 if (code == HttpStatus.SC_OK) { 947 InputStream is = resp.getInputStream(); 948 try { 949 GalParser gp = new GalParser(is, svc); 950 if (gp.parse()) { 951 return gp.getGalResult(); 952 } 953 } finally { 954 is.close(); 955 } 956 } else { 957 svc.userLog("GAL lookup returned " + code); 958 } 959 } finally { 960 resp.close(); 961 } 962 } catch (IOException e) { 963 // GAL is non-critical; we'll just go on 964 svc.userLog("GAL lookup exception " + e); 965 } 966 } 967 return null; 968 } 969 /** 970 * Send an email responding to a Message that has been marked as a meeting request. The message 971 * will consist a little bit of event information and an iCalendar attachment 972 * @param msg the meeting request email 973 */ 974 private void sendMeetingResponseMail(Message msg, int response) { 975 // Get the meeting information; we'd better have some... 976 if (msg.mMeetingInfo == null) return; 977 PackedString meetingInfo = new PackedString(msg.mMeetingInfo); 978 979 // This will come as "First Last" <box@server.blah>, so we use Address to 980 // parse it into parts; we only need the email address part for the ics file 981 Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL)); 982 // It shouldn't be possible, but handle it anyway 983 if (addrs.length != 1) return; 984 String organizerEmail = addrs[0].getAddress(); 985 986 String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP); 987 String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART); 988 String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND); 989 990 // What we're doing here is to create an Entity that looks like an Event as it would be 991 // stored by CalendarProvider 992 ContentValues entityValues = new ContentValues(); 993 Entity entity = new Entity(entityValues); 994 995 // Fill in times, location, title, and organizer 996 entityValues.put("DTSTAMP", 997 CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp)); 998 entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart)); 999 entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd)); 1000 entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION)); 1001 entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE)); 1002 entityValues.put(Events.ORGANIZER, organizerEmail); 1003 1004 // Add ourselves as an attendee, using our account email address 1005 ContentValues attendeeValues = new ContentValues(); 1006 attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, 1007 Attendees.RELATIONSHIP_ATTENDEE); 1008 attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress); 1009 entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); 1010 1011 // Add the organizer 1012 ContentValues organizerValues = new ContentValues(); 1013 organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP, 1014 Attendees.RELATIONSHIP_ORGANIZER); 1015 organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail); 1016 entity.addSubValue(Attendees.CONTENT_URI, organizerValues); 1017 1018 // Create a message from the Entity we've built. The message will have fields like 1019 // to, subject, date, and text filled in. There will also be an "inline" attachment 1020 // which is in iCalendar format 1021 int flag; 1022 switch(response) { 1023 case EmailServiceConstants.MEETING_REQUEST_ACCEPTED: 1024 flag = Message.FLAG_OUTGOING_MEETING_ACCEPT; 1025 break; 1026 case EmailServiceConstants.MEETING_REQUEST_DECLINED: 1027 flag = Message.FLAG_OUTGOING_MEETING_DECLINE; 1028 break; 1029 case EmailServiceConstants.MEETING_REQUEST_TENTATIVE: 1030 default: 1031 flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE; 1032 break; 1033 } 1034 Message outgoingMsg = 1035 CalendarUtilities.createMessageForEntity(mContext, entity, flag, 1036 meetingInfo.get(MeetingInfo.MEETING_UID), mAccount); 1037 // Assuming we got a message back (we might not if the event has been deleted), send it 1038 if (outgoingMsg != null) { 1039 EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg); 1040 } 1041 } 1042 1043 /** 1044 * Responds to a move request. The MessageMoveRequest is basically our 1045 * wrapper for the MoveItems service call 1046 * @param req the request (message id and "to" mailbox id) 1047 * @throws IOException 1048 */ 1049 protected void messageMoveRequest(MessageMoveRequest req) throws IOException { 1050 // Retrieve the message and mailbox; punt if either are null 1051 Message msg = Message.restoreMessageWithId(mContext, req.mMessageId); 1052 if (msg == null) return; 1053 Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, 1054 msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null); 1055 if (c == null) throw new ProviderUnavailableException(); 1056 Mailbox srcMailbox = null; 1057 try { 1058 if (!c.moveToNext()) return; 1059 srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0)); 1060 } finally { 1061 c.close(); 1062 } 1063 if (srcMailbox == null) return; 1064 Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId); 1065 if (dstMailbox == null) return; 1066 Serializer s = new Serializer(); 1067 s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE); 1068 s.data(Tags.MOVE_SRCMSGID, msg.mServerId); 1069 s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId); 1070 s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId); 1071 s.end().end().done(); 1072 EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray()); 1073 try { 1074 int status = resp.getStatus(); 1075 if (status == HttpStatus.SC_OK) { 1076 if (!resp.isEmpty()) { 1077 InputStream is = resp.getInputStream(); 1078 MoveItemsParser p = new MoveItemsParser(is, this); 1079 p.parse(); 1080 int statusCode = p.getStatusCode(); 1081 ContentValues cv = new ContentValues(); 1082 if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) { 1083 // Restore the old mailbox id 1084 cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId); 1085 mContentResolver.update( 1086 ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId), 1087 cv, null, null); 1088 } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) { 1089 // Update with the new server id 1090 cv.put(SyncColumns.SERVER_ID, p.getNewServerId()); 1091 cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE); 1092 mContentResolver.update( 1093 ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId), 1094 cv, null, null); 1095 } 1096 if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS 1097 || statusCode == MoveItemsParser.STATUS_CODE_REVERT) { 1098 // If we revert or succeed, we no longer need the update information 1099 // OR the now-duplicate email (the new copy will be synced down) 1100 mContentResolver.delete(ContentUris.withAppendedId( 1101 Message.UPDATED_CONTENT_URI, req.mMessageId), null, null); 1102 } else { 1103 // In this case, we're retrying, so do nothing. The request will be 1104 // handled next sync 1105 } 1106 } 1107 } else if (EasResponse.isAuthError(status)) { 1108 throw new EasAuthenticationException(); 1109 } else { 1110 userLog("Move items request failed, code: " + status); 1111 throw new IOException(); 1112 } 1113 } finally { 1114 resp.close(); 1115 } 1116 } 1117 1118 /** 1119 * Responds to a meeting request. The MeetingResponseRequest is basically our 1120 * wrapper for the meetingResponse service call 1121 * @param req the request (message id and response code) 1122 * @throws IOException 1123 */ 1124 protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException { 1125 // Retrieve the message and mailbox; punt if either are null 1126 Message msg = Message.restoreMessageWithId(mContext, req.mMessageId); 1127 if (msg == null) return; 1128 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey); 1129 if (mailbox == null) return; 1130 Serializer s = new Serializer(); 1131 s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST); 1132 s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse)); 1133 s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId); 1134 s.data(Tags.MREQ_REQ_ID, msg.mServerId); 1135 s.end().end().done(); 1136 EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray()); 1137 try { 1138 int status = resp.getStatus(); 1139 if (status == HttpStatus.SC_OK) { 1140 if (!resp.isEmpty()) { 1141 InputStream is = resp.getInputStream(); 1142 new MeetingResponseParser(is, this).parse(); 1143 String meetingInfo = msg.mMeetingInfo; 1144 if (meetingInfo != null) { 1145 String responseRequested = new PackedString(meetingInfo).get( 1146 MeetingInfo.MEETING_RESPONSE_REQUESTED); 1147 // If there's no tag, or a non-zero tag, we send the response mail 1148 if ("0".equals(responseRequested)) { 1149 return; 1150 } 1151 } 1152 sendMeetingResponseMail(msg, req.mResponse); 1153 } 1154 } else if (EasResponse.isAuthError(status)) { 1155 throw new EasAuthenticationException(); 1156 } else { 1157 userLog("Meeting response request failed, code: " + status); 1158 throw new IOException(); 1159 } 1160 } finally { 1161 resp.close(); 1162 } 1163 } 1164 1165 /** 1166 * Using mUserName and mPassword, lazily create the strings that are commonly used in our HTTP 1167 * POSTs, including the authentication header string, the base URI we use to communicate with 1168 * EAS, and the user information string (user, deviceId, and deviceType) 1169 */ 1170 private void cacheAuthUserAndBaseUriStrings() { 1171 if (mAuthString == null || mUserString == null || mBaseUriString == null) { 1172 String safeUserName = Uri.encode(mUserName); 1173 String cs = mUserName + ':' + mPassword; 1174 mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP); 1175 mUserString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + 1176 "&DeviceType=" + DEVICE_TYPE; 1177 String scheme = 1178 EmailClientConnectionManager.makeScheme(mSsl, mTrustSsl, mClientCertAlias); 1179 mBaseUriString = scheme + "://" + mHostAddress + "/Microsoft-Server-ActiveSync"; 1180 } 1181 } 1182 1183 @VisibleForTesting 1184 String makeUriString(String cmd, String extra) { 1185 cacheAuthUserAndBaseUriStrings(); 1186 String uriString = mBaseUriString; 1187 if (cmd != null) { 1188 uriString += "?Cmd=" + cmd + mUserString; 1189 } 1190 if (extra != null) { 1191 uriString += extra; 1192 } 1193 return uriString; 1194 } 1195 1196 /** 1197 * Set standard HTTP headers, using a policy key if required 1198 * @param method the method we are going to send 1199 * @param usePolicyKey whether or not a policy key should be sent in the headers 1200 */ 1201 /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) { 1202 method.setHeader("Authorization", mAuthString); 1203 method.setHeader("MS-ASProtocolVersion", mProtocolVersion); 1204 method.setHeader("User-Agent", USER_AGENT); 1205 method.setHeader("Accept-Encoding", "gzip"); 1206 if (usePolicyKey) { 1207 // If there's an account in existence, use its key; otherwise (we're creating the 1208 // account), send "0". The server will respond with code 449 if there are policies 1209 // to be enforced 1210 String key = "0"; 1211 if (mAccount != null) { 1212 String accountKey = mAccount.mSecuritySyncKey; 1213 if (!TextUtils.isEmpty(accountKey)) { 1214 key = accountKey; 1215 } 1216 } 1217 method.setHeader("X-MS-PolicyKey", key); 1218 } 1219 } 1220 1221 protected void setConnectionParameters(HostAuth hostAuth) throws CertificateException { 1222 mSsl = hostAuth.shouldUseSsl(); 1223 mTrustSsl = hostAuth.shouldTrustAllServerCerts(); 1224 mClientCertAlias = hostAuth.mClientCertAlias; 1225 mPort = hostAuth.mPort; 1226 1227 // Register the new alias, if needed. 1228 if (mClientCertAlias != null) { 1229 // Ensure that the connection manager knows to use the proper client certificate 1230 // when establishing connections for this service. 1231 EmailClientConnectionManager connManager = getClientConnectionManager(); 1232 connManager.registerClientCert(mContext, hostAuth); 1233 } 1234 } 1235 1236 private EmailClientConnectionManager getClientConnectionManager() { 1237 return ExchangeService.getClientConnectionManager(mSsl, mPort); 1238 } 1239 1240 private HttpClient getHttpClient(int timeout) { 1241 HttpParams params = new BasicHttpParams(); 1242 HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); 1243 HttpConnectionParams.setSoTimeout(params, timeout); 1244 HttpConnectionParams.setSocketBufferSize(params, 8192); 1245 HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params); 1246 return client; 1247 } 1248 1249 public EasResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException { 1250 return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT); 1251 } 1252 1253 protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException { 1254 return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT); 1255 } 1256 1257 protected EasResponse sendPing(byte[] bytes, int heartbeat) throws IOException { 1258 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 1259 return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS); 1260 } 1261 1262 /** 1263 * Convenience method for executePostWithTimeout for use other than with the Ping command 1264 */ 1265 protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout) 1266 throws IOException { 1267 return executePostWithTimeout(client, method, timeout, false); 1268 } 1269 1270 /** 1271 * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior 1272 * @param client the HttpClient 1273 * @param method the HttpPost 1274 * @param timeout the timeout before failure, in ms 1275 * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic) 1276 * @return the HttpResponse 1277 * @throws IOException 1278 */ 1279 protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout, 1280 boolean isPingCommand) throws IOException { 1281 synchronized(getSynchronizer()) { 1282 mPendingPost = method; 1283 long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE; 1284 if (isPingCommand) { 1285 ExchangeService.runAsleep(mMailboxId, alarmTime); 1286 } else { 1287 ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime); 1288 } 1289 } 1290 try { 1291 return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method); 1292 } finally { 1293 synchronized(getSynchronizer()) { 1294 if (isPingCommand) { 1295 ExchangeService.runAwake(mMailboxId); 1296 } else { 1297 ExchangeService.clearWatchdogAlarm(mMailboxId); 1298 } 1299 mPendingPost = null; 1300 } 1301 } 1302 } 1303 1304 public EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout) 1305 throws IOException { 1306 HttpClient client = getHttpClient(timeout); 1307 boolean isPingCommand = cmd.equals(PING_COMMAND); 1308 1309 // Split the mail sending commands 1310 String extra = null; 1311 boolean msg = false; 1312 if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) { 1313 int cmdLength = cmd.indexOf('&'); 1314 extra = cmd.substring(cmdLength); 1315 cmd = cmd.substring(0, cmdLength); 1316 msg = true; 1317 } else if (cmd.startsWith("SendMail&")) { 1318 msg = true; 1319 } 1320 1321 String us = makeUriString(cmd, extra); 1322 HttpPost method = new HttpPost(URI.create(us)); 1323 // Send the proper Content-Type header; it's always wbxml except for messages when 1324 // the EAS protocol version is < 14.0 1325 // If entity is null (e.g. for attachments), don't set this header 1326 if (msg && (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) { 1327 method.setHeader("Content-Type", "message/rfc822"); 1328 } else if (entity != null) { 1329 method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml"); 1330 } 1331 setHeaders(method, !isPingCommand); 1332 // NOTE 1333 // The next lines are added at the insistence of $VENDOR, who is seeing inappropriate 1334 // network activity related to the Ping command on some networks with some servers. 1335 // This code should be removed when the underlying issue is resolved 1336 if (isPingCommand) { 1337 method.setHeader("Connection", "close"); 1338 } 1339 method.setEntity(entity); 1340 return executePostWithTimeout(client, method, timeout, isPingCommand); 1341 } 1342 1343 protected EasResponse sendHttpClientOptions() throws IOException { 1344 cacheAuthUserAndBaseUriStrings(); 1345 // For OPTIONS, just use the base string and the single header 1346 String uriString = mBaseUriString; 1347 HttpOptions method = new HttpOptions(URI.create(uriString)); 1348 method.setHeader("Authorization", mAuthString); 1349 method.setHeader("User-Agent", USER_AGENT); 1350 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 1351 return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method); 1352 } 1353 1354 String getTargetCollectionClassFromCursor(Cursor c) { 1355 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 1356 if (type == Mailbox.TYPE_CONTACTS) { 1357 return "Contacts"; 1358 } else if (type == Mailbox.TYPE_CALENDAR) { 1359 return "Calendar"; 1360 } else { 1361 return "Email"; 1362 } 1363 } 1364 1365 /** 1366 * Negotiate provisioning with the server. First, get policies form the server and see if 1367 * the policies are supported by the device. Then, write the policies to the account and 1368 * tell SecurityPolicy that we have policies in effect. Finally, see if those policies are 1369 * active; if so, acknowledge the policies to the server and get a final policy key that we 1370 * use in future EAS commands and write this key to the account. 1371 * @return whether or not provisioning has been successful 1372 * @throws IOException 1373 */ 1374 public static boolean tryProvision(EasSyncService svc) throws IOException { 1375 // First, see if provisioning is even possible, i.e. do we support the policies required 1376 // by the server 1377 ProvisionParser pp = canProvision(svc); 1378 if (pp == null) return false; 1379 Context context = svc.mContext; 1380 Account account = svc.mAccount; 1381 // Get the policies from ProvisionParser 1382 Policy policy = pp.getPolicy(); 1383 Policy oldPolicy = null; 1384 // Grab the old policy (if any) 1385 if (svc.mAccount.mPolicyKey > 0) { 1386 oldPolicy = Policy.restorePolicyWithId(context, account.mPolicyKey); 1387 } 1388 // Update the account with a null policyKey (the key we've gotten is 1389 // temporary and cannot be used for syncing) 1390 PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, null); 1391 // Make sure mAccount is current (with latest policy key) 1392 account.refresh(context); 1393 if (pp.getRemoteWipe()) { 1394 // We've gotten a remote wipe command 1395 ExchangeService.alwaysLog("!!! Remote wipe request received"); 1396 // Start by setting the account to security hold 1397 PolicyServiceProxy.setAccountHoldFlag(context, account, true); 1398 // Force a stop to any running syncs for this account (except this one) 1399 ExchangeService.stopNonAccountMailboxSyncsForAccount(account.mId); 1400 1401 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that 1402 // we wipe the device regardless of any errors in acknowledgment 1403 try { 1404 ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server"); 1405 acknowledgeRemoteWipe(svc, pp.getSecuritySyncKey()); 1406 } catch (Exception e) { 1407 // Because remote wipe is such a high priority task, we don't want to 1408 // circumvent it if there's an exception in acknowledgment 1409 } 1410 // Then, tell SecurityPolicy to wipe the device 1411 ExchangeService.alwaysLog("!!! Executing remote wipe"); 1412 PolicyServiceProxy.remoteWipe(context); 1413 return false; 1414 } else if (pp.hasSupportablePolicySet() && PolicyServiceProxy.isActive(context, policy)) { 1415 // See if the required policies are in force; if they are, acknowledge the policies 1416 // to the server and get the final policy key 1417 // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser 1418 String securitySyncKey; 1419 if (svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 1420 securitySyncKey = pp.getSecuritySyncKey(); 1421 } else { 1422 securitySyncKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(), 1423 PROVISION_STATUS_OK); 1424 } 1425 if (securitySyncKey != null) { 1426 // If attachment policies have changed, fix up any affected attachment records 1427 if (oldPolicy != null) { 1428 if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) || 1429 (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) { 1430 Policy.setAttachmentFlagsForNewPolicy(context, account, policy); 1431 } 1432 } 1433 // Write the final policy key to the Account and say we've been successful 1434 PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, securitySyncKey); 1435 // Release any mailboxes that might be in a security hold 1436 ExchangeService.releaseSecurityHold(account); 1437 return true; 1438 } 1439 } 1440 return false; 1441 } 1442 1443 private static String getPolicyType(Double protocolVersion) { 1444 return (protocolVersion >= 1445 Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE; 1446 } 1447 1448 /** 1449 * Obtain a set of policies from the server and determine whether those policies are supported 1450 * by the device. 1451 * @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise 1452 * @throws IOException 1453 */ 1454 public static ProvisionParser canProvision(EasSyncService svc) throws IOException { 1455 Serializer s = new Serializer(); 1456 Double protocolVersion = svc.mProtocolVersionDouble; 1457 s.start(Tags.PROVISION_PROVISION); 1458 if (svc.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) { 1459 // Send settings information in 14.1 and greater 1460 s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET); 1461 s.data(Tags.SETTINGS_MODEL, Build.MODEL); 1462 //s.data(Tags.SETTINGS_IMEI, ""); 1463 //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name"); 1464 s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE); 1465 //s.data(Tags.SETTINGS_OS_LANGUAGE, ""); 1466 //s.data(Tags.SETTINGS_PHONE_NUMBER, ""); 1467 //s.data(Tags.SETTINGS_MOBILE_OPERATOR, ""); 1468 s.data(Tags.SETTINGS_USER_AGENT, EasSyncService.USER_AGENT); 1469 s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION 1470 } 1471 s.start(Tags.PROVISION_POLICIES); 1472 s.start(Tags.PROVISION_POLICY); 1473 s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(protocolVersion)); 1474 s.end().end().end().done(); // PROVISION_POLICY, PROVISION_POLICIES, PROVISION_PROVISION 1475 EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray()); 1476 try { 1477 int code = resp.getStatus(); 1478 if (code == HttpStatus.SC_OK) { 1479 InputStream is = resp.getInputStream(); 1480 ProvisionParser pp = new ProvisionParser(is, svc); 1481 if (pp.parse()) { 1482 // The PolicySet in the ProvisionParser will have the requirements for all KNOWN 1483 // policies. If others are required, hasSupportablePolicySet will be false 1484 if (pp.hasSupportablePolicySet() && 1485 svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 1486 // In EAS 14.0, we need the final security key in order to use the settings 1487 // command 1488 String policyKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(), 1489 PROVISION_STATUS_OK); 1490 if (policyKey != null) { 1491 pp.setSecuritySyncKey(policyKey); 1492 } 1493 } else if (!pp.hasSupportablePolicySet()) { 1494 // Try to acknowledge using the "partial" status (i.e. we can partially 1495 // accommodate the required policies). The server will agree to this if the 1496 // "allow non-provisionable devices" setting is enabled on the server 1497 ExchangeService.log("PolicySet is NOT fully supportable"); 1498 if (acknowledgeProvision(svc, pp.getSecuritySyncKey(), 1499 PROVISION_STATUS_PARTIAL) != null) { 1500 // The server's ok with our inability to support policies, so we'll 1501 // clear them 1502 pp.clearUnsupportablePolicies(); 1503 } 1504 } 1505 return pp; 1506 } 1507 } 1508 } finally { 1509 resp.close(); 1510 } 1511 1512 // On failures, simply return null 1513 return null; 1514 } 1515 1516 /** 1517 * Acknowledge that we support the policies provided by the server, and that these policies 1518 * are in force. 1519 * @param tempKey the initial (temporary) policy key sent by the server 1520 * @return the final policy key, which can be used for syncing 1521 * @throws IOException 1522 */ 1523 private static void acknowledgeRemoteWipe(EasSyncService svc, String tempKey) 1524 throws IOException { 1525 acknowledgeProvisionImpl(svc, tempKey, PROVISION_STATUS_OK, true); 1526 } 1527 1528 private static String acknowledgeProvision(EasSyncService svc, String tempKey, String result) 1529 throws IOException { 1530 return acknowledgeProvisionImpl(svc, tempKey, result, false); 1531 } 1532 1533 private static String acknowledgeProvisionImpl(EasSyncService svc, String tempKey, 1534 String status, boolean remoteWipe) throws IOException { 1535 Serializer s = new Serializer(); 1536 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1537 s.start(Tags.PROVISION_POLICY); 1538 1539 // Use the proper policy type, depending on EAS version 1540 s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(svc.mProtocolVersionDouble)); 1541 1542 s.data(Tags.PROVISION_POLICY_KEY, tempKey); 1543 s.data(Tags.PROVISION_STATUS, status); 1544 s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES 1545 if (remoteWipe) { 1546 s.start(Tags.PROVISION_REMOTE_WIPE); 1547 s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK); 1548 s.end(); 1549 } 1550 s.end().done(); // PROVISION_PROVISION 1551 EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray()); 1552 try { 1553 int code = resp.getStatus(); 1554 if (code == HttpStatus.SC_OK) { 1555 InputStream is = resp.getInputStream(); 1556 ProvisionParser pp = new ProvisionParser(is, svc); 1557 if (pp.parse()) { 1558 // Return the final policy key from the ProvisionParser 1559 String result = (pp.getSecuritySyncKey() == null) ? "failed" : "confirmed"; 1560 ExchangeService.log("Provision " + result + " for " + 1561 (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set"); 1562 return pp.getSecuritySyncKey(); 1563 } 1564 } 1565 } finally { 1566 resp.close(); 1567 } 1568 // On failures, log issue and return null 1569 ExchangeService.log("Provisioning failed for" + 1570 (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set"); 1571 return null; 1572 } 1573 1574 private boolean sendSettings() throws IOException { 1575 Serializer s = new Serializer(); 1576 s.start(Tags.SETTINGS_SETTINGS); 1577 s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET); 1578 s.data(Tags.SETTINGS_MODEL, Build.MODEL); 1579 s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE); 1580 s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT); 1581 s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS 1582 EasResponse resp = sendHttpClientPost("Settings", s.toByteArray()); 1583 try { 1584 int code = resp.getStatus(); 1585 if (code == HttpStatus.SC_OK) { 1586 InputStream is = resp.getInputStream(); 1587 SettingsParser sp = new SettingsParser(is, this); 1588 return sp.parse(); 1589 } 1590 } finally { 1591 resp.close(); 1592 } 1593 // On failures, simply return false 1594 return false; 1595 } 1596 1597 /** 1598 * Common code to sync E+PIM data 1599 * @param target an EasMailbox, EasContacts, or EasCalendar object 1600 */ 1601 public void sync(AbstractSyncAdapter target) throws IOException { 1602 Mailbox mailbox = target.mMailbox; 1603 1604 boolean moreAvailable = true; 1605 int loopingCount = 0; 1606 while (!mStop && (moreAvailable || hasPendingRequests())) { 1607 // If we have no connectivity, just exit cleanly. ExchangeService will start us up again 1608 // when connectivity has returned 1609 if (!hasConnectivity()) { 1610 userLog("No connectivity in sync; finishing sync"); 1611 mExitStatus = EXIT_DONE; 1612 return; 1613 } 1614 1615 // Every time through the loop we check to see if we're still syncable 1616 if (!target.isSyncable()) { 1617 mExitStatus = EXIT_DONE; 1618 return; 1619 } 1620 1621 // Now, handle various requests 1622 while (true) { 1623 Request req = null; 1624 1625 if (mRequestQueue.isEmpty()) { 1626 break; 1627 } else { 1628 req = mRequestQueue.peek(); 1629 } 1630 1631 // Our two request types are PartRequest (loading attachment) and 1632 // MeetingResponseRequest (respond to a meeting request) 1633 if (req instanceof PartRequest) { 1634 TrafficStats.setThreadStatsTag( 1635 TrafficFlags.getAttachmentFlags(mContext, mAccount)); 1636 new AttachmentLoader(this, (PartRequest)req).loadAttachment(); 1637 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, mAccount)); 1638 } else if (req instanceof MeetingResponseRequest) { 1639 sendMeetingResponse((MeetingResponseRequest)req); 1640 } else if (req instanceof MessageMoveRequest) { 1641 messageMoveRequest((MessageMoveRequest)req); 1642 } 1643 1644 // If there's an exception handling the request, we'll throw it 1645 // Otherwise, we remove the request 1646 mRequestQueue.remove(); 1647 } 1648 1649 // Don't sync if we've got nothing to do 1650 if (!moreAvailable) { 1651 continue; 1652 } 1653 1654 Serializer s = new Serializer(); 1655 1656 String className = target.getCollectionName(); 1657 String syncKey = target.getSyncKey(); 1658 userLog("sync, sending ", className, " syncKey: ", syncKey); 1659 s.start(Tags.SYNC_SYNC) 1660 .start(Tags.SYNC_COLLECTIONS) 1661 .start(Tags.SYNC_COLLECTION); 1662 // The "Class" element is removed in EAS 12.1 and later versions 1663 if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) { 1664 s.data(Tags.SYNC_CLASS, className); 1665 } 1666 s.data(Tags.SYNC_SYNC_KEY, syncKey) 1667 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId); 1668 1669 // Start with the default timeout 1670 int timeout = COMMAND_TIMEOUT; 1671 boolean initialSync = syncKey.equals("0"); 1672 // EAS doesn't allow GetChanges in an initial sync; sending other options 1673 // appears to cause the server to delay its response in some cases, and this delay 1674 // can be long enough to result in an IOException and total failure to sync. 1675 // Therefore, we don't send any options with the initial sync. 1676 // Set the truncation amount, body preference, lookback, etc. 1677 target.sendSyncOptions(mProtocolVersionDouble, s, initialSync); 1678 if (initialSync) { 1679 // Use enormous timeout for initial sync, which empirically can take a while longer 1680 timeout = 120*SECONDS; 1681 } 1682 // Send our changes up to the server 1683 if (mUpsyncFailed) { 1684 if (Eas.USER_LOG) { 1685 Log.d(TAG, "Inhibiting upsync this cycle"); 1686 } 1687 } else { 1688 target.sendLocalChanges(s); 1689 } 1690 1691 s.end().end().end().done(); 1692 EasResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()), 1693 timeout); 1694 try { 1695 int code = resp.getStatus(); 1696 if (code == HttpStatus.SC_OK) { 1697 // In EAS 12.1, we can get "empty" sync responses, which indicate that there are 1698 // no changes in the mailbox; handle that case here 1699 // There are two cases here; if we get back a compressed stream (GZIP), we won't 1700 // know until we try to parse it (and generate an EmptyStreamException). If we 1701 // get uncompressed data, the response will be empty (i.e. have zero length) 1702 boolean emptyStream = false; 1703 if (!resp.isEmpty()) { 1704 InputStream is = resp.getInputStream(); 1705 try { 1706 moreAvailable = target.parse(is); 1707 // If we inhibited upsync, we need yet another sync 1708 if (mUpsyncFailed) { 1709 moreAvailable = true; 1710 } 1711 1712 if (target.isLooping()) { 1713 loopingCount++; 1714 userLog("** Looping: " + loopingCount); 1715 // After the maximum number of loops, we'll set moreAvailable to 1716 // false and allow the sync loop to terminate 1717 if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) { 1718 userLog("** Looping force stopped"); 1719 moreAvailable = false; 1720 } 1721 } else { 1722 loopingCount = 0; 1723 } 1724 1725 // Cleanup clears out the updated/deleted tables, and we don't want to 1726 // do that if our upsync failed; clear the flag otherwise 1727 if (!mUpsyncFailed) { 1728 target.cleanup(); 1729 } else { 1730 mUpsyncFailed = false; 1731 } 1732 } catch (EmptyStreamException e) { 1733 userLog("Empty stream detected in GZIP response"); 1734 emptyStream = true; 1735 } catch (CommandStatusException e) { 1736 // TODO 14.1 1737 int status = e.mStatus; 1738 if (CommandStatus.isNeedsProvisioning(status)) { 1739 mExitStatus = EXIT_SECURITY_FAILURE; 1740 } else if (CommandStatus.isDeniedAccess(status)) { 1741 mExitStatus = EXIT_ACCESS_DENIED; 1742 } else if (CommandStatus.isTransientError(status)) { 1743 mExitStatus = EXIT_IO_ERROR; 1744 } else { 1745 mExitStatus = EXIT_EXCEPTION; 1746 } 1747 return; 1748 } 1749 } else { 1750 emptyStream = true; 1751 } 1752 1753 if (emptyStream) { 1754 // If this happens, exit cleanly, and change the interval from push to ping 1755 // if necessary 1756 userLog("Empty sync response; finishing"); 1757 if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) { 1758 userLog("Changing mailbox from push to ping"); 1759 ContentValues cv = new ContentValues(); 1760 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING); 1761 mContentResolver.update( 1762 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId), 1763 cv, null, null); 1764 } 1765 if (mRequestQueue.isEmpty()) { 1766 mExitStatus = EXIT_DONE; 1767 return; 1768 } else { 1769 continue; 1770 } 1771 } 1772 } else { 1773 userLog("Sync response error: ", code); 1774 if (EasResponse.isProvisionError(code)) { 1775 mExitStatus = EXIT_SECURITY_FAILURE; 1776 } else if (EasResponse.isAuthError(code)) { 1777 mExitStatus = EXIT_LOGIN_FAILURE; 1778 } else { 1779 mExitStatus = EXIT_IO_ERROR; 1780 } 1781 return; 1782 } 1783 } finally { 1784 resp.close(); 1785 } 1786 } 1787 mExitStatus = EXIT_DONE; 1788 } 1789 1790 protected boolean setupService() { 1791 synchronized(getSynchronizer()) { 1792 mThread = Thread.currentThread(); 1793 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 1794 TAG = mThread.getName(); 1795 } 1796 // Make sure account and mailbox are always the latest from the database 1797 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 1798 if (mAccount == null) return false; 1799 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 1800 if (mMailbox == null) return false; 1801 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 1802 if (ha == null) return false; 1803 mHostAddress = ha.mAddress; 1804 mUserName = ha.mLogin; 1805 mPassword = ha.mPassword; 1806 1807 try { 1808 setConnectionParameters(ha); 1809 } catch (CertificateException e) { 1810 userLog("Couldn't retrieve certificate for connection"); 1811 try { 1812 ExchangeService.callback().syncMailboxStatus(mMailboxId, 1813 EmailServiceStatus.CLIENT_CERTIFICATE_ERROR, 0); 1814 } catch (RemoteException e1) { 1815 // Don't care if this fails. 1816 } 1817 return false; 1818 } 1819 1820 // Set up our protocol version from the Account 1821 mProtocolVersion = mAccount.mProtocolVersion; 1822 // If it hasn't been set up, start with default version 1823 if (mProtocolVersion == null) { 1824 mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION; 1825 } 1826 mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion); 1827 1828 // Do checks to address historical policy sets. 1829 Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey); 1830 if ((policy != null) && policy.mRequireEncryptionExternal) { 1831 // External storage encryption is not supported at this time. In a previous release, 1832 // prior to the system supporting true removable storage on Honeycomb, we accepted 1833 // this since we emulated external storage on partitions that could be encrypted. 1834 // If that was set before, we must clear it out now that the system supports true 1835 // removable storage (which can't be encrypted). 1836 resetSecurityPolicies(); 1837 } 1838 return true; 1839 } 1840 1841 /** 1842 * Clears out the security policies associated with the account, forcing a provision error 1843 * and a re-sync of the policy information for the account. 1844 */ 1845 @SuppressWarnings("deprecation") 1846 void resetSecurityPolicies() { 1847 ContentValues cv = new ContentValues(); 1848 cv.put(AccountColumns.SECURITY_FLAGS, 0); 1849 cv.putNull(AccountColumns.SECURITY_SYNC_KEY); 1850 long accountId = mAccount.mId; 1851 mContentResolver.update(ContentUris.withAppendedId( 1852 Account.CONTENT_URI, accountId), cv, null, null); 1853 } 1854 1855 @Override 1856 public void run() { 1857 try { 1858 // Make sure account and mailbox are still valid 1859 if (!setupService()) return; 1860 // If we've been stopped, we're done 1861 if (mStop) return; 1862 1863 // Whether or not we're the account mailbox 1864 try { 1865 mDeviceId = ExchangeService.getDeviceId(mContext); 1866 int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount); 1867 if ((mMailbox == null) || (mAccount == null)) { 1868 return; 1869 } else { 1870 AbstractSyncAdapter target; 1871 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) { 1872 TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CONTACTS); 1873 target = new ContactsSyncAdapter( this); 1874 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) { 1875 TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CALENDAR); 1876 target = new CalendarSyncAdapter(this); 1877 } else { 1878 TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL); 1879 target = new EmailSyncAdapter(this); 1880 } 1881 // We loop because someone might have put a request in while we were syncing 1882 // and we've missed that opportunity... 1883 do { 1884 if (mRequestTime != 0) { 1885 userLog("Looping for user request..."); 1886 mRequestTime = 0; 1887 } 1888 String syncKey = target.getSyncKey(); 1889 if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START || 1890 "0".equals(syncKey)) { 1891 try { 1892 ExchangeService.callback().syncMailboxStatus(mMailboxId, 1893 EmailServiceStatus.IN_PROGRESS, 0); 1894 } catch (RemoteException e1) { 1895 // Don't care if this fails 1896 } 1897 } 1898 sync(target); 1899 } while (mRequestTime != 0); 1900 } 1901 } catch (EasAuthenticationException e) { 1902 userLog("Caught authentication error"); 1903 mExitStatus = EXIT_LOGIN_FAILURE; 1904 } catch (IOException e) { 1905 String message = e.getMessage(); 1906 userLog("Caught IOException: ", (message == null) ? "No message" : message); 1907 mExitStatus = EXIT_IO_ERROR; 1908 } catch (Exception e) { 1909 userLog("Uncaught exception in EasSyncService", e); 1910 } finally { 1911 int status; 1912 ExchangeService.done(this); 1913 if (!mStop) { 1914 userLog("Sync finished"); 1915 switch (mExitStatus) { 1916 case EXIT_IO_ERROR: 1917 status = EmailServiceStatus.CONNECTION_ERROR; 1918 break; 1919 case EXIT_DONE: 1920 status = EmailServiceStatus.SUCCESS; 1921 ContentValues cv = new ContentValues(); 1922 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1923 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 1924 cv.put(Mailbox.SYNC_STATUS, s); 1925 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, 1926 mMailboxId), cv, null, null); 1927 break; 1928 case EXIT_LOGIN_FAILURE: 1929 status = EmailServiceStatus.LOGIN_FAILED; 1930 break; 1931 case EXIT_SECURITY_FAILURE: 1932 status = EmailServiceStatus.SECURITY_FAILURE; 1933 // Ask for a new folder list. This should wake up the account mailbox; a 1934 // security error in account mailbox should start provisioning 1935 ExchangeService.reloadFolderList(mContext, mAccount.mId, true); 1936 break; 1937 case EXIT_ACCESS_DENIED: 1938 status = EmailServiceStatus.ACCESS_DENIED; 1939 break; 1940 default: 1941 status = EmailServiceStatus.REMOTE_EXCEPTION; 1942 errorLog("Sync ended due to an exception."); 1943 break; 1944 } 1945 } else { 1946 userLog("Stopped sync finished."); 1947 status = EmailServiceStatus.SUCCESS; 1948 } 1949 1950 // Send a callback (doesn't matter how the sync was started) 1951 try { 1952 // Unless the user specifically asked for a sync, we don't want to report 1953 // connection issues, as they are likely to be transient. In this case, we 1954 // simply report success, so that the progress indicator terminates without 1955 // putting up an error banner 1956 if (mSyncReason != ExchangeService.SYNC_UI_REQUEST && 1957 status == EmailServiceStatus.CONNECTION_ERROR) { 1958 status = EmailServiceStatus.SUCCESS; 1959 } 1960 ExchangeService.callback().syncMailboxStatus(mMailboxId, status, 0); 1961 } catch (RemoteException e1) { 1962 // Don't care if this fails 1963 } 1964 1965 // Make sure ExchangeService knows about this 1966 ExchangeService.kick("sync finished"); 1967 } 1968 } catch (ProviderUnavailableException e) { 1969 Log.e(TAG, "EmailProvider unavailable; sync ended prematurely"); 1970 } 1971 } 1972} 1973