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