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