EasSyncService.java revision 8eeede0d74ba2b139ce96d883541f12a562cc17e
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 com.android.email.SecurityPolicy; 21import com.android.email.Utility; 22import com.android.email.SecurityPolicy.PolicySet; 23import com.android.email.mail.Address; 24import com.android.email.mail.MeetingInfo; 25import com.android.email.mail.MessagingException; 26import com.android.email.mail.PackedString; 27import com.android.email.provider.EmailContent.Account; 28import com.android.email.provider.EmailContent.AccountColumns; 29import com.android.email.provider.EmailContent.Attachment; 30import com.android.email.provider.EmailContent.AttachmentColumns; 31import com.android.email.provider.EmailContent.HostAuth; 32import com.android.email.provider.EmailContent.Mailbox; 33import com.android.email.provider.EmailContent.MailboxColumns; 34import com.android.email.provider.EmailContent.Message; 35import com.android.email.service.EmailServiceConstants; 36import com.android.email.service.EmailServiceProxy; 37import com.android.email.service.EmailServiceStatus; 38import com.android.exchange.adapter.AbstractSyncAdapter; 39import com.android.exchange.adapter.AccountSyncAdapter; 40import com.android.exchange.adapter.CalendarSyncAdapter; 41import com.android.exchange.adapter.ContactsSyncAdapter; 42import com.android.exchange.adapter.EmailSyncAdapter; 43import com.android.exchange.adapter.FolderSyncParser; 44import com.android.exchange.adapter.GalParser; 45import com.android.exchange.adapter.MeetingResponseParser; 46import com.android.exchange.adapter.PingParser; 47import com.android.exchange.adapter.ProvisionParser; 48import com.android.exchange.adapter.Serializer; 49import com.android.exchange.adapter.Tags; 50import com.android.exchange.adapter.Parser.EasParserException; 51import com.android.exchange.provider.GalResult; 52import com.android.exchange.utility.CalendarUtilities; 53 54import org.apache.http.Header; 55import org.apache.http.HttpEntity; 56import org.apache.http.HttpResponse; 57import org.apache.http.HttpStatus; 58import org.apache.http.client.HttpClient; 59import org.apache.http.client.methods.HttpOptions; 60import org.apache.http.client.methods.HttpPost; 61import org.apache.http.client.methods.HttpRequestBase; 62import org.apache.http.conn.ClientConnectionManager; 63import org.apache.http.entity.ByteArrayEntity; 64import org.apache.http.entity.StringEntity; 65import org.apache.http.impl.client.DefaultHttpClient; 66import org.apache.http.params.BasicHttpParams; 67import org.apache.http.params.HttpConnectionParams; 68import org.apache.http.params.HttpParams; 69import org.xmlpull.v1.XmlPullParser; 70import org.xmlpull.v1.XmlPullParserException; 71import org.xmlpull.v1.XmlPullParserFactory; 72import org.xmlpull.v1.XmlSerializer; 73 74import android.content.ContentResolver; 75import android.content.ContentUris; 76import android.content.ContentValues; 77import android.content.Context; 78import android.content.Entity; 79import android.database.Cursor; 80import android.net.Uri; 81import android.os.Bundle; 82import android.os.RemoteException; 83import android.os.SystemClock; 84import android.provider.Calendar.Attendees; 85import android.provider.Calendar.Events; 86import android.text.TextUtils; 87import android.util.Base64; 88import android.util.Log; 89import android.util.Xml; 90 91import java.io.ByteArrayOutputStream; 92import java.io.File; 93import java.io.FileOutputStream; 94import java.io.IOException; 95import java.io.InputStream; 96import java.lang.Thread.State; 97import java.net.URI; 98import java.security.cert.CertificateException; 99import java.util.ArrayList; 100import java.util.HashMap; 101 102public class EasSyncService extends AbstractSyncService { 103 // DO NOT CHECK IN SET TO TRUE 104 public static final boolean DEBUG_GAL_SERVICE = false; 105 106 private static final String EMAIL_WINDOW_SIZE = "5"; 107 public static final String PIM_WINDOW_SIZE = "4"; 108 private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID = 109 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?"; 110 private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING = 111 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 112 '=' + Mailbox.CHECK_INTERVAL_PING; 113 private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " + 114 MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING + 115 ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" + 116 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"'; 117 private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX = 118 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL + 119 '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD; 120 static private final int CHUNK_SIZE = 16*1024; 121 122 static private final String PING_COMMAND = "Ping"; 123 // Command timeout is the the time allowed for reading data from an open connection before an 124 // IOException is thrown. After a small added allowance, our watchdog alarm goes off (allowing 125 // us to detect a silently dropped connection). The allowance is defined below. 126 static private final int COMMAND_TIMEOUT = 20*SECONDS; 127 // Connection timeout is the time given to connect to the server before reporting an IOException 128 static private final int CONNECTION_TIMEOUT = 20*SECONDS; 129 // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers 130 static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS; 131 132 static private final String AUTO_DISCOVER_SCHEMA_PREFIX = 133 "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/"; 134 static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml"; 135 static private final int AUTO_DISCOVER_REDIRECT_CODE = 451; 136 137 static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML"; 138 static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML"; 139 140 /** 141 * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's 142 * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let 143 * the ping exception out. The maximum I use is 17 minutes, which is really an empirical 144 * choice; too long and we risk silent connection loss and loss of push for that period. Too 145 * short and we lose efficiency/battery life. 146 * 147 * If we ever have to drop the ping timeout, we'll never increase it again. There's no point 148 * going into hysteresis; the NAT timeout isn't going to change without a change in connection, 149 * which will cause the sync service to be restarted at the starting heartbeat and going through 150 * the process again. 151 */ 152 static private final int PING_MINUTES = 60; // in seconds 153 static private final int PING_FUDGE_LOW = 10; 154 static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW; 155 static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW; 156 static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW; 157 static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES; 158 static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES; 159 160 // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before 161 // forcing it to stop. This number has been determined empirically. 162 static private final int MAX_LOOPING_COUNT = 100; 163 164 static private final int PROTOCOL_PING_STATUS_COMPLETED = 1; 165 166 // The amount of time we allow for a thread to release its post lock after receiving an alert 167 static private final int POST_LOCK_TIMEOUT = 10*SECONDS; 168 169 // Fallbacks (in minutes) for ping loop failures 170 static private final int MAX_PING_FAILURES = 1; 171 static private final int PING_FALLBACK_INBOX = 5; 172 static private final int PING_FALLBACK_PIM = 25; 173 174 // MSFT's custom HTTP result code indicating the need to provision 175 static private final int HTTP_NEED_PROVISIONING = 449; 176 177 // The EAS protocol Provision status for "we implement all of the policies" 178 static private final String PROVISION_STATUS_OK = "1"; 179 // The EAS protocol Provision status meaning "we partially implement the policies" 180 static private final String PROVISION_STATUS_PARTIAL = "2"; 181 182 // Reasonable default 183 public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION; 184 public Double mProtocolVersionDouble; 185 protected String mDeviceId = null; 186 /*package*/ String mDeviceType = "Android"; 187 /*package*/ String mAuthString = null; 188 /*package*/ String mCmdString = null; 189 public String mHostAddress; 190 public String mUserName; 191 public String mPassword; 192 private boolean mSsl = true; 193 private boolean mTrustSsl = false; 194 public ContentResolver mContentResolver; 195 private String[] mBindArguments = new String[2]; 196 private ArrayList<String> mPingChangeList; 197 // The HttpPost in progress 198 private volatile HttpPost mPendingPost = null; 199 // The ping time (in seconds) 200 private int mPingHeartbeat = PING_STARTING_HEARTBEAT; 201 // The longest successful ping heartbeat 202 private int mPingHighWaterMark = 0; 203 // Whether we've ever lowered the heartbeat 204 private boolean mPingHeartbeatDropped = false; 205 // Whether a POST was aborted due to alarm (watchdog alarm) 206 private boolean mPostAborted = false; 207 // Whether a POST was aborted due to reset 208 private boolean mPostReset = false; 209 // Whether or not the sync service is valid (usable) 210 public boolean mIsValid = true; 211 212 public EasSyncService(Context _context, Mailbox _mailbox) { 213 super(_context, _mailbox); 214 mContentResolver = _context.getContentResolver(); 215 if (mAccount == null) { 216 mIsValid = false; 217 return; 218 } 219 HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv); 220 if (ha == null) { 221 mIsValid = false; 222 return; 223 } 224 mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 225 mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0; 226 } 227 228 private EasSyncService(String prefix) { 229 super(prefix); 230 } 231 232 public EasSyncService() { 233 this("EAS Validation"); 234 } 235 236 @Override 237 /** 238 * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its 239 * socket timeout without having thrown an Exception 240 * 241 * @return true if the POST was successfully stopped; false if we've failed and interrupted 242 * the thread 243 */ 244 public boolean alarm() { 245 HttpPost post; 246 if (mThread == null) return true; 247 String threadName = mThread.getName(); 248 249 // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock 250 // executePostWithTimeout (which executes the HttpPost) also uses this lock 251 synchronized(getSynchronizer()) { 252 // Get a reference to the current post lock 253 post = mPendingPost; 254 if (post != null) { 255 if (Eas.USER_LOG) { 256 URI uri = post.getURI(); 257 if (uri != null) { 258 String query = uri.getQuery(); 259 if (query == null) { 260 query = "POST"; 261 } 262 userLog(threadName, ": Alert, aborting ", query); 263 } else { 264 userLog(threadName, ": Alert, no URI?"); 265 } 266 } 267 // Abort the POST 268 mPostAborted = true; 269 post.abort(); 270 } else { 271 // If there's no POST, we're done 272 userLog("Alert, no pending POST"); 273 return true; 274 } 275 } 276 277 // Wait for the POST to finish 278 try { 279 Thread.sleep(POST_LOCK_TIMEOUT); 280 } catch (InterruptedException e) { 281 } 282 283 State s = mThread.getState(); 284 if (Eas.USER_LOG) { 285 userLog(threadName + ": State = " + s.name()); 286 } 287 288 synchronized (getSynchronizer()) { 289 // If the thread is still hanging around and the same post is pending, let's try to 290 // stop the thread with an interrupt. 291 if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) { 292 mStop = true; 293 mThread.interrupt(); 294 userLog("Interrupting..."); 295 // Let the caller know we had to interrupt the thread 296 return false; 297 } 298 } 299 // Let the caller know that the alarm was handled normally 300 return true; 301 } 302 303 @Override 304 public void reset() { 305 synchronized(getSynchronizer()) { 306 if (mPendingPost != null) { 307 URI uri = mPendingPost.getURI(); 308 if (uri != null) { 309 String query = uri.getQuery(); 310 if (query.startsWith("Cmd=Ping")) { 311 userLog("Reset, aborting Ping"); 312 mPostReset = true; 313 mPendingPost.abort(); 314 } 315 } 316 } 317 } 318 } 319 320 @Override 321 public void stop() { 322 mStop = true; 323 synchronized(getSynchronizer()) { 324 if (mPendingPost != null) { 325 mPendingPost.abort(); 326 } 327 } 328 } 329 330 /** 331 * Determine whether an HTTP code represents an authentication error 332 * @param code the HTTP code returned by the server 333 * @return whether or not the code represents an authentication error 334 */ 335 protected boolean isAuthError(int code) { 336 return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN); 337 } 338 339 /** 340 * Determine whether an HTTP code represents a provisioning error 341 * @param code the HTTP code returned by the server 342 * @return whether or not the code represents an provisioning error 343 */ 344 protected boolean isProvisionError(int code) { 345 return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN); 346 } 347 348 private void setupProtocolVersion(EasSyncService service, Header versionHeader) 349 throws MessagingException { 350 // The string is a comma separated list of EAS versions in ascending order 351 // e.g. 1.0,2.0,2.5,12.0,12.1 352 String supportedVersions = versionHeader.getValue(); 353 userLog("Server supports versions: ", supportedVersions); 354 String[] supportedVersionsArray = supportedVersions.split(","); 355 String ourVersion = null; 356 // Find the most recent version we support 357 for (String version: supportedVersionsArray) { 358 if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) || 359 version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) || 360 version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1)) { 361 ourVersion = version; 362 } 363 } 364 // If we don't support any of the servers supported versions, throw an exception here 365 // This will cause validation to fail 366 if (ourVersion == null) { 367 Log.w(TAG, "No supported EAS versions: " + supportedVersions); 368 throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED); 369 } else { 370 service.mProtocolVersion = ourVersion; 371 service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion); 372 if (service.mAccount != null) { 373 service.mAccount.mProtocolVersion = ourVersion; 374 } 375 } 376 } 377 378 @Override 379 public Bundle validateAccount(String hostAddress, String userName, String password, int port, 380 boolean ssl, boolean trustCertificates, Context context) { 381 Bundle bundle = new Bundle(); 382 int resultCode = MessagingException.NO_ERROR; 383 try { 384 userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0"); 385 EasSyncService svc = new EasSyncService("%TestAccount%"); 386 svc.mContext = context; 387 svc.mHostAddress = hostAddress; 388 svc.mUserName = userName; 389 svc.mPassword = password; 390 svc.mSsl = ssl; 391 svc.mTrustSsl = trustCertificates; 392 // We mustn't use the "real" device id or we'll screw up current accounts 393 // Any string will do, but we'll go for "validate" 394 svc.mDeviceId = "validate"; 395 HttpResponse resp = svc.sendHttpClientOptions(); 396 int code = resp.getStatusLine().getStatusCode(); 397 userLog("Validation (OPTIONS) response: " + code); 398 if (code == HttpStatus.SC_OK) { 399 // No exception means successful validation 400 Header commands = resp.getFirstHeader("MS-ASProtocolCommands"); 401 Header versions = resp.getFirstHeader("ms-asprotocolversions"); 402 // Make sure we've got the right protocol version set up 403 try { 404 if (commands == null || versions == null) { 405 userLog("OPTIONS response without commands or versions"); 406 // We'll treat this as a protocol exception 407 throw new MessagingException(0); 408 } 409 setupProtocolVersion(svc, versions); 410 } catch (MessagingException e) { 411 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, 412 MessagingException.PROTOCOL_VERSION_UNSUPPORTED); 413 return bundle; 414 } 415 416 // Run second test here for provisioning failures using FolderSync 417 userLog("Try folder sync"); 418 // Send "0" as the sync key for new accounts; otherwise we'll use the current key 419 String syncKey = "0"; 420 Account existingAccount = 421 Utility.findExistingAccount(context, -1L, hostAddress, userName); 422 if (existingAccount != null && existingAccount.mSyncKey != null) { 423 syncKey = existingAccount.mSyncKey; 424 } 425 Serializer s = new Serializer(); 426 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey) 427 .end().end().done(); 428 resp = svc.sendHttpClientPost("FolderSync", s.toByteArray()); 429 code = resp.getStatusLine().getStatusCode(); 430 // We'll get one of the following responses if policies are required by the server 431 if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) { 432 // Get the policies and see if we are able to support them 433 ProvisionParser pp = svc.canProvision(); 434 if (pp != null) { 435 // If so, set the proper result code and save the PolicySet in our Bundle 436 resultCode = MessagingException.SECURITY_POLICIES_REQUIRED; 437 bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET, 438 pp.getPolicySet()); 439 } else 440 // If not, set the proper code (the account will not be created) 441 resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED; 442 } else if (code == HttpStatus.SC_NOT_FOUND) { 443 // We get a 404 from OWA addresses (which are NOT EAS addresses) 444 resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED; 445 } else if (code != HttpStatus.SC_OK) { 446 // Fail generically with anything other than success 447 userLog("Unexpected response for FolderSync: ", code); 448 resultCode = MessagingException.UNSPECIFIED_EXCEPTION; 449 } else { 450 userLog("Validation successful"); 451 } 452 } else if (isAuthError(code)) { 453 userLog("Authentication failed"); 454 resultCode = MessagingException.AUTHENTICATION_FAILED; 455 } else { 456 // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code. 457 userLog("Validation failed, reporting I/O error: ", code); 458 resultCode = MessagingException.IOERROR; 459 } 460 } catch (IOException e) { 461 Throwable cause = e.getCause(); 462 if (cause != null && cause instanceof CertificateException) { 463 userLog("CertificateException caught: ", e.getMessage()); 464 resultCode = MessagingException.GENERAL_SECURITY; 465 } 466 userLog("IOException caught: ", e.getMessage()); 467 resultCode = MessagingException.IOERROR; 468 } 469 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode); 470 return bundle; 471 } 472 473 /** 474 * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that 475 * it can be reused 476 * 477 * @param resp the HttpResponse that indicates a redirect (451) 478 * @param post the HttpPost that was originally sent to the server 479 * @return the HttpPost, updated with the redirect location 480 */ 481 private HttpPost getRedirect(HttpResponse resp, HttpPost post) { 482 Header locHeader = resp.getFirstHeader("X-MS-Location"); 483 if (locHeader != null) { 484 String loc = locHeader.getValue(); 485 // If we've gotten one and it shows signs of looking like an address, we try 486 // sending our request there 487 if (loc != null && loc.startsWith("http")) { 488 post.setURI(URI.create(loc)); 489 return post; 490 } 491 } 492 return null; 493 } 494 495 /** 496 * Send the POST command to the autodiscover server, handling a redirect, if necessary, and 497 * return the HttpResponse. If we get a 401 (unauthorized) error and we're using the 498 * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com) 499 * 500 * @param client the HttpClient to be used for the request 501 * @param post the HttpPost we're going to send 502 * @param canRetry whether we can retry using the bare name on an authentication failure (401) 503 * @return an HttpResponse from the original or redirect server 504 * @throws IOException on any IOException within the HttpClient code 505 * @throws MessagingException 506 */ 507 private HttpResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry) 508 throws IOException, MessagingException { 509 userLog("Posting autodiscover to: " + post.getURI()); 510 HttpResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT); 511 int code = resp.getStatusLine().getStatusCode(); 512 // On a redirect, try the new location 513 if (code == AUTO_DISCOVER_REDIRECT_CODE) { 514 post = getRedirect(resp, post); 515 if (post != null) { 516 userLog("Posting autodiscover to redirect: " + post.getURI()); 517 return executePostWithTimeout(client, post, COMMAND_TIMEOUT); 518 } 519 // 401 (Unauthorized) is for true auth errors when used in Autodiscover 520 } else if (code == HttpStatus.SC_UNAUTHORIZED) { 521 if (canRetry && mUserName.contains("@")) { 522 // Try again using the bare user name 523 int atSignIndex = mUserName.indexOf('@'); 524 mUserName = mUserName.substring(0, atSignIndex); 525 cacheAuthAndCmdString(); 526 userLog("401 received; trying username: ", mUserName); 527 // Recreate the basic authentication string and reset the header 528 post.removeHeaders("Authorization"); 529 post.setHeader("Authorization", mAuthString); 530 return postAutodiscover(client, post, false); 531 } 532 throw new MessagingException(MessagingException.AUTHENTICATION_FAILED); 533 // 403 (and others) we'll just punt on 534 } else if (code != HttpStatus.SC_OK) { 535 // We'll try the next address if this doesn't work 536 userLog("Code: " + code + ", throwing IOException"); 537 throw new IOException(); 538 } 539 return resp; 540 } 541 542 /** 543 * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using 544 * only an email address and the password 545 * 546 * @param userName the user's email address 547 * @param password the user's password 548 * @return a HostAuth ready to be saved in an Account or null (failure) 549 */ 550 public Bundle tryAutodiscover(String userName, String password) throws RemoteException { 551 XmlSerializer s = Xml.newSerializer(); 552 ByteArrayOutputStream os = new ByteArrayOutputStream(1024); 553 HostAuth hostAuth = new HostAuth(); 554 Bundle bundle = new Bundle(); 555 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 556 MessagingException.NO_ERROR); 557 try { 558 // Build the XML document that's sent to the autodiscover server(s) 559 s.setOutput(os, "UTF-8"); 560 s.startDocument("UTF-8", false); 561 s.startTag(null, "Autodiscover"); 562 s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006"); 563 s.startTag(null, "Request"); 564 s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress"); 565 s.startTag(null, "AcceptableResponseSchema"); 566 s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006"); 567 s.endTag(null, "AcceptableResponseSchema"); 568 s.endTag(null, "Request"); 569 s.endTag(null, "Autodiscover"); 570 s.endDocument(); 571 String req = os.toString(); 572 573 // Initialize the user name and password 574 mUserName = userName; 575 mPassword = password; 576 // Make sure the authentication string is recreated and cached 577 cacheAuthAndCmdString(); 578 579 // Split out the domain name 580 int amp = userName.indexOf('@'); 581 // The UI ensures that userName is a valid email address 582 if (amp < 0) { 583 throw new RemoteException(); 584 } 585 String domain = userName.substring(amp + 1); 586 587 // There are up to four attempts here; the two URLs that we're supposed to try per the 588 // specification, and up to one redirect for each (handled in postAutodiscover) 589 // Note: The expectation is that, of these four attempts, only a single server will 590 // actually be identified as the autodiscover server. For the identified server, 591 // we may also try a 2nd connection with a different format (bare name). 592 593 // Try the domain first and see if we can get a response 594 HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE); 595 setHeaders(post, false); 596 post.setHeader("Content-Type", "text/xml"); 597 post.setEntity(new StringEntity(req)); 598 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 599 HttpResponse resp; 600 try { 601 resp = postAutodiscover(client, post, true /*canRetry*/); 602 } catch (IOException e1) { 603 userLog("IOException in autodiscover; trying alternate address"); 604 // We catch the IOException here because we have an alternate address to try 605 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE)); 606 // If we fail here, we're out of options, so we let the outer try catch the 607 // IOException and return null 608 resp = postAutodiscover(client, post, true /*canRetry*/); 609 } 610 611 // Get the "final" code; if it's not 200, just return null 612 int code = resp.getStatusLine().getStatusCode(); 613 userLog("Code: " + code); 614 if (code != HttpStatus.SC_OK) return null; 615 616 // At this point, we have a 200 response (SC_OK) 617 HttpEntity e = resp.getEntity(); 618 InputStream is = e.getContent(); 619 try { 620 // The response to Autodiscover is regular XML (not WBXML) 621 // If we ever get an error in this process, we'll just punt and return null 622 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 623 XmlPullParser parser = factory.newPullParser(); 624 parser.setInput(is, "UTF-8"); 625 int type = parser.getEventType(); 626 if (type == XmlPullParser.START_DOCUMENT) { 627 type = parser.next(); 628 if (type == XmlPullParser.START_TAG) { 629 String name = parser.getName(); 630 if (name.equals("Autodiscover")) { 631 hostAuth = new HostAuth(); 632 parseAutodiscover(parser, hostAuth); 633 // On success, we'll have a server address and login 634 if (hostAuth.mAddress != null) { 635 // Fill in the rest of the HostAuth 636 // We use the user name and password that were successful during 637 // the autodiscover process 638 hostAuth.mLogin = mUserName; 639 hostAuth.mPassword = mPassword; 640 hostAuth.mPort = 443; 641 hostAuth.mProtocol = "eas"; 642 hostAuth.mFlags = 643 HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE; 644 bundle.putParcelable( 645 EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth); 646 } else { 647 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 648 MessagingException.UNSPECIFIED_EXCEPTION); 649 } 650 } 651 } 652 } 653 } catch (XmlPullParserException e1) { 654 // This would indicate an I/O error of some sort 655 // We will simply return null and user can configure manually 656 } 657 // There's no reason at all for exceptions to be thrown, and it's ok if so. 658 // We just won't do auto-discover; user can configure manually 659 } catch (IllegalArgumentException e) { 660 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 661 MessagingException.UNSPECIFIED_EXCEPTION); 662 } catch (IllegalStateException e) { 663 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 664 MessagingException.UNSPECIFIED_EXCEPTION); 665 } catch (IOException e) { 666 userLog("IOException in Autodiscover", e); 667 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 668 MessagingException.IOERROR); 669 } catch (MessagingException e) { 670 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE, 671 MessagingException.AUTHENTICATION_FAILED); 672 } 673 return bundle; 674 } 675 676 void parseServer(XmlPullParser parser, HostAuth hostAuth) 677 throws XmlPullParserException, IOException { 678 boolean mobileSync = false; 679 while (true) { 680 int type = parser.next(); 681 if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) { 682 break; 683 } else if (type == XmlPullParser.START_TAG) { 684 String name = parser.getName(); 685 if (name.equals("Type")) { 686 if (parser.nextText().equals("MobileSync")) { 687 mobileSync = true; 688 } 689 } else if (mobileSync && name.equals("Url")) { 690 String url = parser.nextText().toLowerCase(); 691 // This will look like https://<server address>/Microsoft-Server-ActiveSync 692 // We need to extract the <server address> 693 if (url.startsWith("https://") && 694 url.endsWith("/microsoft-server-activesync")) { 695 int lastSlash = url.lastIndexOf('/'); 696 hostAuth.mAddress = url.substring(8, lastSlash); 697 userLog("Autodiscover, server: " + hostAuth.mAddress); 698 } 699 } 700 } 701 } 702 } 703 704 void parseSettings(XmlPullParser parser, HostAuth hostAuth) 705 throws XmlPullParserException, IOException { 706 while (true) { 707 int type = parser.next(); 708 if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) { 709 break; 710 } else if (type == XmlPullParser.START_TAG) { 711 String name = parser.getName(); 712 if (name.equals("Server")) { 713 parseServer(parser, hostAuth); 714 } 715 } 716 } 717 } 718 719 void parseAction(XmlPullParser parser, HostAuth hostAuth) 720 throws XmlPullParserException, IOException { 721 while (true) { 722 int type = parser.next(); 723 if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) { 724 break; 725 } else if (type == XmlPullParser.START_TAG) { 726 String name = parser.getName(); 727 if (name.equals("Error")) { 728 // Should parse the error 729 } else if (name.equals("Redirect")) { 730 Log.d(TAG, "Redirect: " + parser.nextText()); 731 } else if (name.equals("Settings")) { 732 parseSettings(parser, hostAuth); 733 } 734 } 735 } 736 } 737 738 void parseUser(XmlPullParser parser, HostAuth hostAuth) 739 throws XmlPullParserException, IOException { 740 while (true) { 741 int type = parser.next(); 742 if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) { 743 break; 744 } else if (type == XmlPullParser.START_TAG) { 745 String name = parser.getName(); 746 if (name.equals("EMailAddress")) { 747 String addr = parser.nextText(); 748 userLog("Autodiscover, email: " + addr); 749 } else if (name.equals("DisplayName")) { 750 String dn = parser.nextText(); 751 userLog("Autodiscover, user: " + dn); 752 } 753 } 754 } 755 } 756 757 void parseResponse(XmlPullParser parser, HostAuth hostAuth) 758 throws XmlPullParserException, IOException { 759 while (true) { 760 int type = parser.next(); 761 if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) { 762 break; 763 } else if (type == XmlPullParser.START_TAG) { 764 String name = parser.getName(); 765 if (name.equals("User")) { 766 parseUser(parser, hostAuth); 767 } else if (name.equals("Action")) { 768 parseAction(parser, hostAuth); 769 } 770 } 771 } 772 } 773 774 void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth) 775 throws XmlPullParserException, IOException { 776 while (true) { 777 int type = parser.nextTag(); 778 if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) { 779 break; 780 } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) { 781 parseResponse(parser, hostAuth); 782 } 783 } 784 } 785 786 /** 787 * Contact the GAL and obtain a list of matching accounts 788 * @param context caller's context 789 * @param accountId the account Id to search 790 * @param filter the characters entered so far 791 * @return a result record or null 792 * 793 * TODO: shorter timeout for interactive lookup 794 * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0) 795 * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion 796 */ 797 static public GalResult searchGal(Context context, long accountId, String filter) { 798 Account acct = SyncManager.getAccountById(accountId); 799 if (acct != null) { 800 HostAuth ha = HostAuth.restoreHostAuthWithId(context, acct.mHostAuthKeyRecv); 801 EasSyncService svc = new EasSyncService("%GalLookupk%"); 802 try { 803 // If there's no protocol version set up, we haven't successfully started syncing 804 // so we can't use GAL yet 805 String protocolVersion = acct.mProtocolVersion; 806 if (protocolVersion == null) { 807 return null; 808 } else { 809 svc.mProtocolVersion = protocolVersion; 810 svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion); 811 } 812 svc.mContext = context; 813 svc.mHostAddress = ha.mAddress; 814 svc.mUserName = ha.mLogin; 815 svc.mPassword = ha.mPassword; 816 svc.mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; 817 svc.mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0; 818 svc.mDeviceId = SyncManager.getDeviceId(); 819 svc.mAccount = acct; 820 Serializer s = new Serializer(); 821 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE); 822 s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter); 823 s.start(Tags.SEARCH_OPTIONS); 824 s.data(Tags.SEARCH_RANGE, "0-19"); // Return 0..20 results 825 s.end().end().end().done(); 826 if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup starting for " + ha.mAddress); 827 HttpResponse resp = svc.sendHttpClientPost("Search", s.toByteArray()); 828 int code = resp.getStatusLine().getStatusCode(); 829 if (code == HttpStatus.SC_OK) { 830 InputStream is = resp.getEntity().getContent(); 831 GalParser gp = new GalParser(is, svc); 832 if (gp.parse()) { 833 if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup OK for " + ha.mAddress); 834 return gp.getGalResult(); 835 } else { 836 if (DEBUG_GAL_SERVICE) svc.userLog("GAL lookup returned no matches"); 837 } 838 } else { 839 svc.userLog("GAL lookup returned " + code); 840 } 841 } catch (IOException e) { 842 // GAL is non-critical; we'll just go on 843 svc.userLog("GAL lookup exception " + e); 844 } 845 } 846 return null; 847 } 848 849 private void doStatusCallback(long messageId, long attachmentId, int status) { 850 try { 851 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); 852 } catch (RemoteException e) { 853 // No danger if the client is no longer around 854 } 855 } 856 857 private void doProgressCallback(long messageId, long attachmentId, int progress) { 858 try { 859 SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, 860 EmailServiceStatus.IN_PROGRESS, progress); 861 } catch (RemoteException e) { 862 // No danger if the client is no longer around 863 } 864 } 865 866 public File createUniqueFileInternal(String dir, String filename) { 867 File directory; 868 if (dir == null) { 869 directory = mContext.getFilesDir(); 870 } else { 871 directory = new File(dir); 872 } 873 if (!directory.exists()) { 874 directory.mkdirs(); 875 } 876 File file = new File(directory, filename); 877 if (!file.exists()) { 878 return file; 879 } 880 // Get the extension of the file, if any. 881 int index = filename.lastIndexOf('.'); 882 String name = filename; 883 String extension = ""; 884 if (index != -1) { 885 name = filename.substring(0, index); 886 extension = filename.substring(index); 887 } 888 for (int i = 2; i < Integer.MAX_VALUE; i++) { 889 file = new File(directory, name + '-' + i + extension); 890 if (!file.exists()) { 891 return file; 892 } 893 } 894 return null; 895 } 896 897 /** 898 * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our 899 * wrapper for Attachment 900 * @param req the part (attachment) to be retrieved 901 * @throws IOException 902 */ 903 protected void getAttachment(PartRequest req) throws IOException { 904 Attachment att = req.mAttachment; 905 Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey); 906 doProgressCallback(msg.mId, att.mId, 0); 907 908 String cmd = "GetAttachment&AttachmentName=" + att.mLocation; 909 HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT); 910 911 int status = res.getStatusLine().getStatusCode(); 912 if (status == HttpStatus.SC_OK) { 913 HttpEntity e = res.getEntity(); 914 int len = (int)e.getContentLength(); 915 InputStream is = res.getEntity().getContent(); 916 File f = (req.mDestination != null) 917 ? new File(req.mDestination) 918 : createUniqueFileInternal(req.mDestination, att.mFileName); 919 if (f != null) { 920 // Ensure that the target directory exists 921 File destDir = f.getParentFile(); 922 if (!destDir.exists()) { 923 destDir.mkdirs(); 924 } 925 FileOutputStream os = new FileOutputStream(f); 926 // len > 0 means that Content-Length was set in the headers 927 // len < 0 means "chunked" transfer-encoding 928 if (len != 0) { 929 try { 930 mPendingRequest = req; 931 byte[] bytes = new byte[CHUNK_SIZE]; 932 int length = len; 933 // Loop terminates 1) when EOF is reached or 2) if an IOException occurs 934 // One of these is guaranteed to occur 935 int totalRead = 0; 936 userLog("Attachment content-length: ", len); 937 while (true) { 938 int read = is.read(bytes, 0, CHUNK_SIZE); 939 940 // read < 0 means that EOF was reached 941 if (read < 0) { 942 userLog("Attachment load reached EOF, totalRead: ", totalRead); 943 break; 944 } 945 946 // Keep track of how much we've read for progress callback 947 totalRead += read; 948 949 // Write these bytes out 950 os.write(bytes, 0, read); 951 952 // We can't report percentages if this is chunked; by definition, the 953 // length of incoming data is unknown 954 if (length > 0) { 955 // Belt and suspenders check to prevent runaway reading 956 if (totalRead > length) { 957 errorLog("totalRead is greater than attachment length?"); 958 break; 959 } 960 int pct = (totalRead * 100) / length; 961 doProgressCallback(msg.mId, att.mId, pct); 962 } 963 } 964 } finally { 965 mPendingRequest = null; 966 } 967 } 968 os.flush(); 969 os.close(); 970 971 // EmailProvider will throw an exception if we try to update an unsaved attachment 972 if (att.isSaved()) { 973 String contentUriString = (req.mContentUriString != null) 974 ? req.mContentUriString 975 : "file://" + f.getAbsolutePath(); 976 ContentValues cv = new ContentValues(); 977 cv.put(AttachmentColumns.CONTENT_URI, contentUriString); 978 att.update(mContext, cv); 979 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS); 980 } 981 } 982 } else { 983 doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND); 984 } 985 } 986 987 /** 988 * Send an email responding to a Message that has been marked as a meeting request. The message 989 * will consist a little bit of event information and an iCalendar attachment 990 * @param msg the meeting request email 991 */ 992 private void sendMeetingResponseMail(Message msg, int response) { 993 // Get the meeting information; we'd better have some... 994 PackedString meetingInfo = new PackedString(msg.mMeetingInfo); 995 if (meetingInfo == null) return; 996 997 // This will come as "First Last" <box@server.blah>, so we use Address to 998 // parse it into parts; we only need the email address part for the ics file 999 Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL)); 1000 // It shouldn't be possible, but handle it anyway 1001 if (addrs.length != 1) return; 1002 String organizerEmail = addrs[0].getAddress(); 1003 1004 String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP); 1005 String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART); 1006 String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND); 1007 1008 // What we're doing here is to create an Entity that looks like an Event as it would be 1009 // stored by CalendarProvider 1010 ContentValues entityValues = new ContentValues(); 1011 Entity entity = new Entity(entityValues); 1012 1013 // Fill in times, location, title, and organizer 1014 entityValues.put("DTSTAMP", 1015 CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp)); 1016 entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart)); 1017 entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd)); 1018 entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION)); 1019 entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE)); 1020 entityValues.put(Events.ORGANIZER, organizerEmail); 1021 1022 // Add ourselves as an attendee, using our account email address 1023 ContentValues attendeeValues = new ContentValues(); 1024 attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, 1025 Attendees.RELATIONSHIP_ATTENDEE); 1026 attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress); 1027 entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); 1028 1029 // Add the organizer 1030 ContentValues organizerValues = new ContentValues(); 1031 organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP, 1032 Attendees.RELATIONSHIP_ORGANIZER); 1033 organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail); 1034 entity.addSubValue(Attendees.CONTENT_URI, organizerValues); 1035 1036 // Create a message from the Entity we've built. The message will have fields like 1037 // to, subject, date, and text filled in. There will also be an "inline" attachment 1038 // which is in iCalendar format 1039 int flag; 1040 switch(response) { 1041 case EmailServiceConstants.MEETING_REQUEST_ACCEPTED: 1042 flag = Message.FLAG_OUTGOING_MEETING_ACCEPT; 1043 break; 1044 case EmailServiceConstants.MEETING_REQUEST_DECLINED: 1045 flag = Message.FLAG_OUTGOING_MEETING_DECLINE; 1046 break; 1047 case EmailServiceConstants.MEETING_REQUEST_TENTATIVE: 1048 default: 1049 flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE; 1050 break; 1051 } 1052 Message outgoingMsg = 1053 CalendarUtilities.createMessageForEntity(mContext, entity, flag, 1054 meetingInfo.get(MeetingInfo.MEETING_UID), mAccount); 1055 // Assuming we got a message back (we might not if the event has been deleted), send it 1056 if (outgoingMsg != null) { 1057 EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg); 1058 } 1059 } 1060 1061 /** 1062 * Responds to a meeting request. The MeetingResponseRequest is basically our 1063 * wrapper for the meetingResponse service call 1064 * @param req the request (message id and response code) 1065 * @throws IOException 1066 */ 1067 protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException { 1068 // Retrieve the message and mailbox; punt if either are null 1069 Message msg = Message.restoreMessageWithId(mContext, req.mMessageId); 1070 if (msg == null) return; 1071 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey); 1072 if (mailbox == null) return; 1073 Serializer s = new Serializer(); 1074 s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST); 1075 s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse)); 1076 s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId); 1077 s.data(Tags.MREQ_REQ_ID, msg.mServerId); 1078 s.end().end().done(); 1079 HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray()); 1080 int status = res.getStatusLine().getStatusCode(); 1081 if (status == HttpStatus.SC_OK) { 1082 HttpEntity e = res.getEntity(); 1083 int len = (int)e.getContentLength(); 1084 InputStream is = res.getEntity().getContent(); 1085 if (len != 0) { 1086 new MeetingResponseParser(is, this).parse(); 1087 sendMeetingResponseMail(msg, req.mResponse); 1088 } 1089 } else if (isAuthError(status)) { 1090 throw new EasAuthenticationException(); 1091 } else { 1092 userLog("Meeting response request failed, code: " + status); 1093 throw new IOException(); 1094 } 1095 } 1096 1097 /** 1098 * Using mUserName and mPassword, create and cache mAuthString and mCacheString, which are used 1099 * in all HttpPost commands. This should be called if these strings are null, or if mUserName 1100 * and/or mPassword are changed 1101 */ 1102 private void cacheAuthAndCmdString() { 1103 String safeUserName = Uri.encode(mUserName); 1104 String cs = mUserName + ':' + mPassword; 1105 mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP); 1106 mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + 1107 "&DeviceType=" + mDeviceType; 1108 } 1109 1110 /*package*/ String makeUriString(String cmd, String extra) throws IOException { 1111 // Cache the authentication string and the command string 1112 if (mAuthString == null || mCmdString == null) { 1113 cacheAuthAndCmdString(); 1114 } 1115 String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress + 1116 "/Microsoft-Server-ActiveSync"; 1117 if (cmd != null) { 1118 us += "?Cmd=" + cmd + mCmdString; 1119 } 1120 if (extra != null) { 1121 us += extra; 1122 } 1123 return us; 1124 } 1125 1126 /** 1127 * Set standard HTTP headers, using a policy key if required 1128 * @param method the method we are going to send 1129 * @param usePolicyKey whether or not a policy key should be sent in the headers 1130 */ 1131 /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) { 1132 method.setHeader("Authorization", mAuthString); 1133 method.setHeader("MS-ASProtocolVersion", mProtocolVersion); 1134 method.setHeader("Connection", "keep-alive"); 1135 method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION); 1136 if (usePolicyKey) { 1137 // If there's an account in existence, use its key; otherwise (we're creating the 1138 // account), send "0". The server will respond with code 449 if there are policies 1139 // to be enforced 1140 String key = "0"; 1141 if (mAccount != null) { 1142 String accountKey = mAccount.mSecuritySyncKey; 1143 if (!TextUtils.isEmpty(accountKey)) { 1144 key = accountKey; 1145 } 1146 } 1147 method.setHeader("X-MS-PolicyKey", key); 1148 } 1149 } 1150 1151 private ClientConnectionManager getClientConnectionManager() { 1152 return SyncManager.getClientConnectionManager(); 1153 } 1154 1155 private HttpClient getHttpClient(int timeout) { 1156 HttpParams params = new BasicHttpParams(); 1157 HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); 1158 HttpConnectionParams.setSoTimeout(params, timeout); 1159 HttpConnectionParams.setSocketBufferSize(params, 8192); 1160 HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params); 1161 return client; 1162 } 1163 1164 protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException { 1165 return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT); 1166 } 1167 1168 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException { 1169 return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT); 1170 } 1171 1172 protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException { 1173 Thread.currentThread().setName(mAccount.mDisplayName + ": Ping"); 1174 if (Eas.USER_LOG) { 1175 userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's'); 1176 } 1177 return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS); 1178 } 1179 1180 /** 1181 * Convenience method for executePostWithTimeout for use other than with the Ping command 1182 */ 1183 protected HttpResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout) 1184 throws IOException { 1185 return executePostWithTimeout(client, method, timeout, false); 1186 } 1187 1188 /** 1189 * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior 1190 * @param client the HttpClient 1191 * @param method the HttpPost 1192 * @param timeout the timeout before failure, in ms 1193 * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic) 1194 * @return the HttpResponse 1195 * @throws IOException 1196 */ 1197 protected HttpResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout, 1198 boolean isPingCommand) throws IOException { 1199 synchronized(getSynchronizer()) { 1200 mPendingPost = method; 1201 long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE; 1202 if (isPingCommand) { 1203 SyncManager.runAsleep(mMailboxId, alarmTime); 1204 } else { 1205 SyncManager.setWatchdogAlarm(mMailboxId, alarmTime); 1206 } 1207 } 1208 try { 1209 return client.execute(method); 1210 } finally { 1211 synchronized(getSynchronizer()) { 1212 if (isPingCommand) { 1213 SyncManager.runAwake(mMailboxId); 1214 } else { 1215 SyncManager.clearWatchdogAlarm(mMailboxId); 1216 } 1217 mPendingPost = null; 1218 } 1219 } 1220 } 1221 1222 protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout) 1223 throws IOException { 1224 HttpClient client = getHttpClient(timeout); 1225 boolean isPingCommand = cmd.equals(PING_COMMAND); 1226 1227 // Split the mail sending commands 1228 String extra = null; 1229 boolean msg = false; 1230 if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) { 1231 int cmdLength = cmd.indexOf('&'); 1232 extra = cmd.substring(cmdLength); 1233 cmd = cmd.substring(0, cmdLength); 1234 msg = true; 1235 } else if (cmd.startsWith("SendMail&")) { 1236 msg = true; 1237 } 1238 1239 String us = makeUriString(cmd, extra); 1240 HttpPost method = new HttpPost(URI.create(us)); 1241 // Send the proper Content-Type header 1242 // If entity is null (e.g. for attachments), don't set this header 1243 if (msg) { 1244 method.setHeader("Content-Type", "message/rfc822"); 1245 } else if (entity != null) { 1246 method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml"); 1247 } 1248 setHeaders(method, !cmd.equals(PING_COMMAND)); 1249 method.setEntity(entity); 1250 return executePostWithTimeout(client, method, timeout, isPingCommand); 1251 } 1252 1253 protected HttpResponse sendHttpClientOptions() throws IOException { 1254 HttpClient client = getHttpClient(COMMAND_TIMEOUT); 1255 String us = makeUriString("OPTIONS", null); 1256 HttpOptions method = new HttpOptions(URI.create(us)); 1257 setHeaders(method, false); 1258 return client.execute(method); 1259 } 1260 1261 String getTargetCollectionClassFromCursor(Cursor c) { 1262 int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 1263 if (type == Mailbox.TYPE_CONTACTS) { 1264 return "Contacts"; 1265 } else if (type == Mailbox.TYPE_CALENDAR) { 1266 return "Calendar"; 1267 } else { 1268 return "Email"; 1269 } 1270 } 1271 1272 /** 1273 * Negotiate provisioning with the server. First, get policies form the server and see if 1274 * the policies are supported by the device. Then, write the policies to the account and 1275 * tell SecurityPolicy that we have policies in effect. Finally, see if those policies are 1276 * active; if so, acknowledge the policies to the server and get a final policy key that we 1277 * use in future EAS commands and write this key to the account. 1278 * @return whether or not provisioning has been successful 1279 * @throws IOException 1280 */ 1281 private boolean tryProvision() throws IOException { 1282 // First, see if provisioning is even possible, i.e. do we support the policies required 1283 // by the server 1284 ProvisionParser pp = canProvision(); 1285 if (pp != null) { 1286 SecurityPolicy sp = SecurityPolicy.getInstance(mContext); 1287 // Get the policies from ProvisionParser 1288 PolicySet ps = pp.getPolicySet(); 1289 // Update the account with a null policyKey (the key we've gotten is 1290 // temporary and cannot be used for syncing) 1291 if (ps.writeAccount(mAccount, null, true, mContext)) { 1292 sp.updatePolicies(mAccount.mId); 1293 } 1294 if (pp.getRemoteWipe()) { 1295 // We've gotten a remote wipe command 1296 // If we're not the admin, we can't do the wipe, so just return 1297 if (!sp.isActiveAdmin()) return false; 1298 // First, we've got to acknowledge it, but wrap the wipe in try/catch so that 1299 // we wipe the device regardless of any errors in acknowledgment 1300 try { 1301 acknowledgeRemoteWipe(pp.getPolicyKey()); 1302 } catch (Exception e) { 1303 // Because remote wipe is such a high priority task, we don't want to 1304 // circumvent it if there's an exception in acknowledgment 1305 } 1306 // Then, tell SecurityPolicy to wipe the device 1307 sp.remoteWipe(); 1308 return false; 1309 } else if (sp.isActive(ps)) { 1310 // See if the required policies are in force; if they are, acknowledge the policies 1311 // to the server and get the final policy key 1312 String policyKey = acknowledgeProvision(pp.getPolicyKey(), PROVISION_STATUS_OK); 1313 if (policyKey != null) { 1314 // Write the final policy key to the Account and say we've been successful 1315 ps.writeAccount(mAccount, policyKey, true, mContext); 1316 return true; 1317 } 1318 } else { 1319 // Notify that we are blocked because of policies 1320 sp.policiesRequired(mAccount.mId); 1321 } 1322 } 1323 return false; 1324 } 1325 1326 private String getPolicyType() { 1327 return (mProtocolVersionDouble >= 1328 Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE; 1329 } 1330 1331 /** 1332 * Obtain a set of policies from the server and determine whether those policies are supported 1333 * by the device. 1334 * @return the ProvisionParser (holds policies and key) if we receive policies and they are 1335 * supported by the device; null otherwise 1336 * @throws IOException 1337 */ 1338 private ProvisionParser canProvision() throws IOException { 1339 Serializer s = new Serializer(); 1340 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1341 s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()) 1342 .end().end().end().done(); 1343 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1344 int code = resp.getStatusLine().getStatusCode(); 1345 if (code == HttpStatus.SC_OK) { 1346 InputStream is = resp.getEntity().getContent(); 1347 ProvisionParser pp = new ProvisionParser(is, this); 1348 if (pp.parse()) { 1349 // The PolicySet in the ProvisionParser will have the requirements for all KNOWN 1350 // policies. If others are required, hasSupportablePolicySet will be false 1351 if (pp.hasSupportablePolicySet()) { 1352 // If the policies are supportable (in this context, meaning that there are no 1353 // completely unimplemented policies required), just return the parser itself 1354 return pp; 1355 } else { 1356 // Try to acknowledge using the "partial" status (i.e. we can partially 1357 // accommodate the required policies). The server will agree to this if the 1358 // "allow non-provisionable devices" setting is enabled on the server 1359 String policyKey = acknowledgeProvision(pp.getPolicyKey(), 1360 PROVISION_STATUS_PARTIAL); 1361 // Return either the parser (success) or null (failure) 1362 return (policyKey != null) ? pp : null; 1363 } 1364 } 1365 } 1366 // On failures, simply return null 1367 return null; 1368 } 1369 1370 /** 1371 * Acknowledge that we support the policies provided by the server, and that these policies 1372 * are in force. 1373 * @param tempKey the initial (temporary) policy key sent by the server 1374 * @return the final policy key, which can be used for syncing 1375 * @throws IOException 1376 */ 1377 private void acknowledgeRemoteWipe(String tempKey) throws IOException { 1378 acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true); 1379 } 1380 1381 private String acknowledgeProvision(String tempKey, String result) throws IOException { 1382 return acknowledgeProvisionImpl(tempKey, result, false); 1383 } 1384 1385 private String acknowledgeProvisionImpl(String tempKey, String status, 1386 boolean remoteWipe) throws IOException { 1387 Serializer s = new Serializer(); 1388 s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES); 1389 s.start(Tags.PROVISION_POLICY); 1390 1391 // Use the proper policy type, depending on EAS version 1392 s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType()); 1393 1394 s.data(Tags.PROVISION_POLICY_KEY, tempKey); 1395 s.data(Tags.PROVISION_STATUS, status); 1396 s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES 1397 if (remoteWipe) { 1398 s.start(Tags.PROVISION_REMOTE_WIPE); 1399 s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK); 1400 s.end(); 1401 } 1402 s.end().done(); // PROVISION_PROVISION 1403 HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray()); 1404 int code = resp.getStatusLine().getStatusCode(); 1405 if (code == HttpStatus.SC_OK) { 1406 InputStream is = resp.getEntity().getContent(); 1407 ProvisionParser pp = new ProvisionParser(is, this); 1408 if (pp.parse()) { 1409 // Return the final policy key from the ProvisionParser 1410 return pp.getPolicyKey(); 1411 } 1412 } 1413 // On failures, return null 1414 return null; 1415 } 1416 1417 /** 1418 * Performs FolderSync 1419 * 1420 * @throws IOException 1421 * @throws EasParserException 1422 */ 1423 public void runAccountMailbox() throws IOException, EasParserException { 1424 // Initialize exit status to success 1425 mExitStatus = EXIT_DONE; 1426 try { 1427 try { 1428 SyncManager.callback() 1429 .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0); 1430 } catch (RemoteException e1) { 1431 // Don't care if this fails 1432 } 1433 1434 // Don't run if we're being held 1435 if ((mAccount.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 1436 mExitStatus = EXIT_SECURITY_FAILURE; 1437 return; 1438 } 1439 1440 if (mAccount.mSyncKey == null) { 1441 mAccount.mSyncKey = "0"; 1442 userLog("Account syncKey INIT to 0"); 1443 ContentValues cv = new ContentValues(); 1444 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 1445 mAccount.update(mContext, cv); 1446 } 1447 1448 boolean firstSync = mAccount.mSyncKey.equals("0"); 1449 if (firstSync) { 1450 userLog("Initial FolderSync"); 1451 } 1452 1453 // When we first start up, change all mailboxes to push. 1454 ContentValues cv = new ContentValues(); 1455 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1456 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1457 WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING, 1458 new String[] {Long.toString(mAccount.mId)}) > 0) { 1459 SyncManager.kick("change ping boxes to push"); 1460 } 1461 1462 // Determine our protocol version, if we haven't already and save it in the Account 1463 // Also re-check protocol version at least once a day (in case of upgrade) 1464 if (mAccount.mProtocolVersion == null || 1465 ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) { 1466 userLog("Determine EAS protocol version"); 1467 HttpResponse resp = sendHttpClientOptions(); 1468 int code = resp.getStatusLine().getStatusCode(); 1469 userLog("OPTIONS response: ", code); 1470 if (code == HttpStatus.SC_OK) { 1471 Header header = resp.getFirstHeader("MS-ASProtocolCommands"); 1472 userLog(header.getValue()); 1473 header = resp.getFirstHeader("ms-asprotocolversions"); 1474 try { 1475 setupProtocolVersion(this, header); 1476 } catch (MessagingException e) { 1477 // Since we've already validated, this can't really happen 1478 // But if it does, we'll rethrow this... 1479 throw new IOException(); 1480 } 1481 // Save the protocol version 1482 cv.clear(); 1483 // Save the protocol version in the account 1484 cv.put(Account.PROTOCOL_VERSION, mProtocolVersion); 1485 mAccount.update(mContext, cv); 1486 cv.clear(); 1487 // Save the sync time of the account mailbox to current time 1488 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 1489 mMailbox.update(mContext, cv); 1490 } else { 1491 errorLog("OPTIONS command failed; throwing IOException"); 1492 throw new IOException(); 1493 } 1494 } 1495 1496 // Change all pushable boxes to push when we start the account mailbox 1497 if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 1498 cv.clear(); 1499 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH); 1500 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1501 SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE, 1502 new String[] {Long.toString(mAccount.mId)}) > 0) { 1503 userLog("Push account; set pushable boxes to push..."); 1504 } 1505 } 1506 1507 while (!mStop) { 1508 userLog("Sending Account syncKey: ", mAccount.mSyncKey); 1509 Serializer s = new Serializer(); 1510 s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY) 1511 .text(mAccount.mSyncKey).end().end().done(); 1512 HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray()); 1513 if (mStop) break; 1514 int code = resp.getStatusLine().getStatusCode(); 1515 if (code == HttpStatus.SC_OK) { 1516 HttpEntity entity = resp.getEntity(); 1517 int len = (int)entity.getContentLength(); 1518 if (len != 0) { 1519 InputStream is = entity.getContent(); 1520 // Returns true if we need to sync again 1521 if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this)) 1522 .parse()) { 1523 continue; 1524 } 1525 } 1526 } else if (isProvisionError(code)) { 1527 // If the sync error is a provisioning failure (perhaps the policies changed), 1528 // let's try the provisioning procedure 1529 // Provisioning must only be attempted for the account mailbox - trying to 1530 // provision any other mailbox may result in race conditions and the creation 1531 // of multiple policy keys. 1532 if (!tryProvision()) { 1533 // Set the appropriate failure status 1534 mExitStatus = EXIT_SECURITY_FAILURE; 1535 return; 1536 } else { 1537 // If we succeeded, try again... 1538 continue; 1539 } 1540 } else if (isAuthError(code)) { 1541 mExitStatus = EXIT_LOGIN_FAILURE; 1542 return; 1543 } else { 1544 userLog("FolderSync response error: ", code); 1545 } 1546 1547 // Change all push/hold boxes to push 1548 cv.clear(); 1549 cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); 1550 if (mContentResolver.update(Mailbox.CONTENT_URI, cv, 1551 WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX, 1552 new String[] {Long.toString(mAccount.mId)}) > 0) { 1553 userLog("Set push/hold boxes to push..."); 1554 } 1555 1556 try { 1557 SyncManager.callback() 1558 .syncMailboxListStatus(mAccount.mId, mExitStatus, 0); 1559 } catch (RemoteException e1) { 1560 // Don't care if this fails 1561 } 1562 1563 // Before each run of the pingLoop, if this Account has a PolicySet, make sure it's 1564 // active; otherwise, clear out the key/flag. This should cause a provisioning 1565 // error on the next POST, and start the security sequence over again 1566 String key = mAccount.mSecuritySyncKey; 1567 if (!TextUtils.isEmpty(key)) { 1568 PolicySet ps = new PolicySet(mAccount); 1569 SecurityPolicy sp = SecurityPolicy.getInstance(mContext); 1570 if (!sp.isActive(ps)) { 1571 cv.clear(); 1572 cv.put(AccountColumns.SECURITY_FLAGS, 0); 1573 cv.putNull(AccountColumns.SECURITY_SYNC_KEY); 1574 long accountId = mAccount.mId; 1575 mContentResolver.update(ContentUris.withAppendedId( 1576 Account.CONTENT_URI, accountId), cv, null, null); 1577 sp.policiesRequired(accountId); 1578 } 1579 } 1580 1581 // Wait for push notifications. 1582 String threadName = Thread.currentThread().getName(); 1583 try { 1584 runPingLoop(); 1585 } catch (StaleFolderListException e) { 1586 // We break out if we get told about a stale folder list 1587 userLog("Ping interrupted; folder list requires sync..."); 1588 } finally { 1589 Thread.currentThread().setName(threadName); 1590 } 1591 } 1592 } catch (IOException e) { 1593 // We catch this here to send the folder sync status callback 1594 // A folder sync failed callback will get sent from run() 1595 try { 1596 if (!mStop) { 1597 SyncManager.callback() 1598 .syncMailboxListStatus(mAccount.mId, 1599 EmailServiceStatus.CONNECTION_ERROR, 0); 1600 } 1601 } catch (RemoteException e1) { 1602 // Don't care if this fails 1603 } 1604 throw e; 1605 } 1606 } 1607 1608 private void pushFallback(long mailboxId) { 1609 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1610 if (mailbox == null) { 1611 return; 1612 } 1613 ContentValues cv = new ContentValues(); 1614 int mins = PING_FALLBACK_PIM; 1615 if (mailbox.mType == Mailbox.TYPE_INBOX) { 1616 mins = PING_FALLBACK_INBOX; 1617 } 1618 cv.put(Mailbox.SYNC_INTERVAL, mins); 1619 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 1620 cv, null, null); 1621 errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync"); 1622 SyncManager.kick("push fallback"); 1623 } 1624 1625 /** 1626 * Simplistic attempt to determine a NAT timeout, based on experience with various carriers 1627 * and networks. The string "reset by peer" is very common in these situations, so we look for 1628 * that specifically. We may add additional tests here as more is learned. 1629 * @param message 1630 * @return whether this message is likely associated with a NAT failure 1631 */ 1632 private boolean isLikelyNatFailure(String message) { 1633 if (message == null) return false; 1634 if (message.contains("reset by peer")) { 1635 return true; 1636 } 1637 return false; 1638 } 1639 1640 private void runPingLoop() throws IOException, StaleFolderListException { 1641 int pingHeartbeat = mPingHeartbeat; 1642 userLog("runPingLoop"); 1643 // Do push for all sync services here 1644 long endTime = System.currentTimeMillis() + (30*MINUTES); 1645 HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>(); 1646 ArrayList<String> readyMailboxes = new ArrayList<String>(); 1647 ArrayList<String> notReadyMailboxes = new ArrayList<String>(); 1648 int pingWaitCount = 0; 1649 1650 while ((System.currentTimeMillis() < endTime) && !mStop) { 1651 // Count of pushable mailboxes 1652 int pushCount = 0; 1653 // Count of mailboxes that can be pushed right now 1654 int canPushCount = 0; 1655 // Count of uninitialized boxes 1656 int uninitCount = 0; 1657 1658 Serializer s = new Serializer(); 1659 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1660 MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId + 1661 AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null); 1662 notReadyMailboxes.clear(); 1663 readyMailboxes.clear(); 1664 try { 1665 // Loop through our pushed boxes seeing what is available to push 1666 while (c.moveToNext()) { 1667 pushCount++; 1668 // Two requirements for push: 1669 // 1) SyncManager tells us the mailbox is syncable (not running, not stopped) 1670 // 2) The syncKey isn't "0" (i.e. it's synced at least once) 1671 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 1672 int pingStatus = SyncManager.pingStatus(mailboxId); 1673 String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1674 if (pingStatus == SyncManager.PING_STATUS_OK) { 1675 String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN); 1676 if ((syncKey == null) || syncKey.equals("0")) { 1677 // We can't push until the initial sync is done 1678 pushCount--; 1679 uninitCount++; 1680 continue; 1681 } 1682 1683 if (canPushCount++ == 0) { 1684 // Initialize the Ping command 1685 s.start(Tags.PING_PING) 1686 .data(Tags.PING_HEARTBEAT_INTERVAL, 1687 Integer.toString(pingHeartbeat)) 1688 .start(Tags.PING_FOLDERS); 1689 } 1690 1691 String folderClass = getTargetCollectionClassFromCursor(c); 1692 s.start(Tags.PING_FOLDER) 1693 .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN)) 1694 .data(Tags.PING_CLASS, folderClass) 1695 .end(); 1696 readyMailboxes.add(mailboxName); 1697 } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) || 1698 (pingStatus == SyncManager.PING_STATUS_WAITING)) { 1699 notReadyMailboxes.add(mailboxName); 1700 } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) { 1701 pushCount--; 1702 userLog(mailboxName, " in error state; ignore"); 1703 continue; 1704 } 1705 } 1706 } finally { 1707 c.close(); 1708 } 1709 1710 if (Eas.USER_LOG) { 1711 if (!notReadyMailboxes.isEmpty()) { 1712 userLog("Ping not ready for: " + notReadyMailboxes); 1713 } 1714 if (!readyMailboxes.isEmpty()) { 1715 userLog("Ping ready for: " + readyMailboxes); 1716 } 1717 } 1718 1719 // If we've waited 10 seconds or more, just ping with whatever boxes are ready 1720 // But use a shorter than normal heartbeat 1721 boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5); 1722 1723 if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) { 1724 // If all pingable boxes are ready for push, send Ping to the server 1725 s.end().end().done(); 1726 pingWaitCount = 0; 1727 mPostReset = false; 1728 mPostAborted = false; 1729 1730 // If we've been stopped, this is a good time to return 1731 if (mStop) return; 1732 1733 long pingTime = SystemClock.elapsedRealtime(); 1734 try { 1735 // Send the ping, wrapped by appropriate timeout/alarm 1736 if (forcePing) { 1737 userLog("Forcing ping after waiting for all boxes to be ready"); 1738 } 1739 HttpResponse res = 1740 sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat); 1741 1742 int code = res.getStatusLine().getStatusCode(); 1743 userLog("Ping response: ", code); 1744 1745 // Return immediately if we've been asked to stop during the ping 1746 if (mStop) { 1747 userLog("Stopping pingLoop"); 1748 return; 1749 } 1750 1751 if (code == HttpStatus.SC_OK) { 1752 // Make sure to clear out any pending sync errors 1753 SyncManager.removeFromSyncErrorMap(mMailboxId); 1754 HttpEntity e = res.getEntity(); 1755 int len = (int)e.getContentLength(); 1756 InputStream is = res.getEntity().getContent(); 1757 if (len != 0) { 1758 int pingResult = parsePingResult(is, mContentResolver, pingErrorMap); 1759 // If our ping completed (status = 1), and we weren't forced and we're 1760 // not at the maximum, try increasing timeout by two minutes 1761 if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) { 1762 if (pingHeartbeat > mPingHighWaterMark) { 1763 mPingHighWaterMark = pingHeartbeat; 1764 userLog("Setting high water mark at: ", mPingHighWaterMark); 1765 } 1766 if ((pingHeartbeat < PING_MAX_HEARTBEAT) && 1767 !mPingHeartbeatDropped) { 1768 pingHeartbeat += PING_HEARTBEAT_INCREMENT; 1769 if (pingHeartbeat > PING_MAX_HEARTBEAT) { 1770 pingHeartbeat = PING_MAX_HEARTBEAT; 1771 } 1772 userLog("Increasing ping heartbeat to ", pingHeartbeat, "s"); 1773 } 1774 } 1775 } else { 1776 userLog("Ping returned empty result; throwing IOException"); 1777 throw new IOException(); 1778 } 1779 } else if (isAuthError(code)) { 1780 mExitStatus = EXIT_LOGIN_FAILURE; 1781 userLog("Authorization error during Ping: ", code); 1782 throw new IOException(); 1783 } 1784 } catch (IOException e) { 1785 String message = e.getMessage(); 1786 // If we get the exception that is indicative of a NAT timeout and if we 1787 // haven't yet "fixed" the timeout, back off by two minutes and "fix" it 1788 boolean hasMessage = message != null; 1789 userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]")); 1790 if (mPostReset) { 1791 // Nothing to do in this case; this is SyncManager telling us to try another 1792 // ping. 1793 } else if (mPostAborted || isLikelyNatFailure(message)) { 1794 long pingLength = SystemClock.elapsedRealtime() - pingTime; 1795 if ((pingHeartbeat > PING_MIN_HEARTBEAT) && 1796 (pingHeartbeat > mPingHighWaterMark)) { 1797 pingHeartbeat -= PING_HEARTBEAT_INCREMENT; 1798 mPingHeartbeatDropped = true; 1799 if (pingHeartbeat < PING_MIN_HEARTBEAT) { 1800 pingHeartbeat = PING_MIN_HEARTBEAT; 1801 } 1802 userLog("Decreased ping heartbeat to ", pingHeartbeat, "s"); 1803 } else if (mPostAborted) { 1804 // There's no point in throwing here; this can happen in two cases 1805 // 1) An alarm, which indicates minutes without activity; no sense 1806 // backing off 1807 // 2) SyncManager abort, due to sync of mailbox. Again, we want to 1808 // keep on trying to ping 1809 userLog("Ping aborted; retry"); 1810 } else if (pingLength < 2000) { 1811 userLog("Abort or NAT type return < 2 seconds; throwing IOException"); 1812 throw e; 1813 } else { 1814 userLog("NAT type IOException"); 1815 } 1816 } else if (hasMessage && message.contains("roken pipe")) { 1817 // The "broken pipe" error (uppercase or lowercase "b") seems to be an 1818 // internal error, so let's not throw an exception (which leads to delays) 1819 // but rather simply run through the loop again 1820 } else { 1821 throw e; 1822 } 1823 } 1824 } else if (forcePing) { 1825 // In this case, there aren't any boxes that are pingable, but there are boxes 1826 // waiting (for IOExceptions) 1827 userLog("pingLoop waiting 60s for any pingable boxes"); 1828 sleep(60*SECONDS, true); 1829 } else if (pushCount > 0) { 1830 // If we want to Ping, but can't just yet, wait a little bit 1831 // TODO Change sleep to wait and use notify from SyncManager when a sync ends 1832 sleep(2*SECONDS, false); 1833 pingWaitCount++; 1834 //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)"); 1835 } else if (uninitCount > 0) { 1836 // In this case, we're doing an initial sync of at least one mailbox. Since this 1837 // is typically a one-time case, I'm ok with trying again every 10 seconds until 1838 // we're in one of the other possible states. 1839 userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)"); 1840 sleep(10*SECONDS, true); 1841 } else { 1842 // We've got nothing to do, so we'll check again in 30 minutes at which time 1843 // we'll update the folder list. Let the device sleep in the meantime... 1844 userLog("pingLoop sleeping for 30m"); 1845 sleep(30*MINUTES, true); 1846 } 1847 } 1848 1849 // Save away the current heartbeat 1850 mPingHeartbeat = pingHeartbeat; 1851 } 1852 1853 private void sleep(long ms, boolean runAsleep) { 1854 if (runAsleep) { 1855 SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS)); 1856 } 1857 try { 1858 Thread.sleep(ms); 1859 } catch (InterruptedException e) { 1860 // Doesn't matter whether we stop early; it's the thought that counts 1861 } finally { 1862 if (runAsleep) { 1863 SyncManager.runAwake(mMailboxId); 1864 } 1865 } 1866 } 1867 1868 private int parsePingResult(InputStream is, ContentResolver cr, 1869 HashMap<String, Integer> errorMap) 1870 throws IOException, StaleFolderListException { 1871 PingParser pp = new PingParser(is, this); 1872 if (pp.parse()) { 1873 // True indicates some mailboxes need syncing... 1874 // syncList has the serverId's of the mailboxes... 1875 mBindArguments[0] = Long.toString(mAccount.mId); 1876 mPingChangeList = pp.getSyncList(); 1877 for (String serverId: mPingChangeList) { 1878 mBindArguments[1] = serverId; 1879 Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 1880 WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null); 1881 try { 1882 if (c.moveToFirst()) { 1883 1884 /** 1885 * Check the boxes reporting changes to see if there really were any... 1886 * We do this because bugs in various Exchange servers can put us into a 1887 * looping behavior by continually reporting changes in a mailbox, even when 1888 * there aren't any. 1889 * 1890 * This behavior is seemingly random, and therefore we must code defensively 1891 * by backing off of push behavior when it is detected. 1892 * 1893 * One known cause, on certain Exchange 2003 servers, is acknowledged by 1894 * Microsoft, and the server hotfix for this case can be found at 1895 * http://support.microsoft.com/kb/923282 1896 */ 1897 1898 // Check the status of the last sync 1899 String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN); 1900 int type = SyncManager.getStatusType(status); 1901 // This check should always be true... 1902 if (type == SyncManager.SYNC_PING) { 1903 int changeCount = SyncManager.getStatusChangeCount(status); 1904 if (changeCount > 0) { 1905 errorMap.remove(serverId); 1906 } else if (changeCount == 0) { 1907 // This means that a ping reported changes in error; we keep a count 1908 // of consecutive errors of this kind 1909 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 1910 Integer failures = errorMap.get(serverId); 1911 if (failures == null) { 1912 userLog("Last ping reported changes in error for: ", name); 1913 errorMap.put(serverId, 1); 1914 } else if (failures > MAX_PING_FAILURES) { 1915 // We'll back off of push for this box 1916 pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN)); 1917 continue; 1918 } else { 1919 userLog("Last ping reported changes in error for: ", name); 1920 errorMap.put(serverId, failures + 1); 1921 } 1922 } 1923 } 1924 1925 // If there were no problems with previous sync, we'll start another one 1926 SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN), 1927 SyncManager.SYNC_PING, null); 1928 } 1929 } finally { 1930 c.close(); 1931 } 1932 } 1933 } 1934 return pp.getSyncStatus(); 1935 } 1936 1937 private String getEmailFilter() { 1938 String filter = Eas.FILTER_1_WEEK; 1939 switch (mAccount.mSyncLookback) { 1940 case com.android.email.Account.SYNC_WINDOW_1_DAY: { 1941 filter = Eas.FILTER_1_DAY; 1942 break; 1943 } 1944 case com.android.email.Account.SYNC_WINDOW_3_DAYS: { 1945 filter = Eas.FILTER_3_DAYS; 1946 break; 1947 } 1948 case com.android.email.Account.SYNC_WINDOW_1_WEEK: { 1949 filter = Eas.FILTER_1_WEEK; 1950 break; 1951 } 1952 case com.android.email.Account.SYNC_WINDOW_2_WEEKS: { 1953 filter = Eas.FILTER_2_WEEKS; 1954 break; 1955 } 1956 case com.android.email.Account.SYNC_WINDOW_1_MONTH: { 1957 filter = Eas.FILTER_1_MONTH; 1958 break; 1959 } 1960 case com.android.email.Account.SYNC_WINDOW_ALL: { 1961 filter = Eas.FILTER_ALL; 1962 break; 1963 } 1964 } 1965 return filter; 1966 } 1967 1968 /** 1969 * Common code to sync E+PIM data 1970 * 1971 * @param target, an EasMailbox, EasContacts, or EasCalendar object 1972 */ 1973 public void sync(AbstractSyncAdapter target) throws IOException { 1974 Mailbox mailbox = target.mMailbox; 1975 1976 boolean moreAvailable = true; 1977 int loopingCount = 0; 1978 while (!mStop && moreAvailable) { 1979 // If we have no connectivity, just exit cleanly. SyncManager will start us up again 1980 // when connectivity has returned 1981 if (!hasConnectivity()) { 1982 userLog("No connectivity in sync; finishing sync"); 1983 mExitStatus = EXIT_DONE; 1984 return; 1985 } 1986 1987 // Every time through the loop we check to see if we're still syncable 1988 if (!target.isSyncable()) { 1989 mExitStatus = EXIT_DONE; 1990 return; 1991 } 1992 1993 // Now, handle various requests 1994 while (true) { 1995 Request req = null; 1996 synchronized (mRequests) { 1997 if (mRequests.isEmpty()) { 1998 break; 1999 } else { 2000 req = mRequests.get(0); 2001 } 2002 } 2003 2004 // Our two request types are PartRequest (loading attachment) and 2005 // MeetingResponseRequest (respond to a meeting request) 2006 if (req instanceof PartRequest) { 2007 getAttachment((PartRequest)req); 2008 } else if (req instanceof MeetingResponseRequest) { 2009 sendMeetingResponse((MeetingResponseRequest)req); 2010 } 2011 2012 // If there's an exception handling the request, we'll throw it 2013 // Otherwise, we remove the request 2014 synchronized(mRequests) { 2015 mRequests.remove(req); 2016 } 2017 } 2018 2019 Serializer s = new Serializer(); 2020 2021 String className = target.getCollectionName(); 2022 String syncKey = target.getSyncKey(); 2023 userLog("sync, sending ", className, " syncKey: ", syncKey); 2024 s.start(Tags.SYNC_SYNC) 2025 .start(Tags.SYNC_COLLECTIONS) 2026 .start(Tags.SYNC_COLLECTION); 2027 // The "Class" element is removed in EAS 12.1 and later versions 2028 if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) { 2029 s.data(Tags.SYNC_CLASS, className); 2030 } 2031 s.data(Tags.SYNC_SYNC_KEY, syncKey) 2032 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId); 2033 2034 // Start with the default timeout 2035 int timeout = COMMAND_TIMEOUT; 2036 if (!syncKey.equals("0")) { 2037 // EAS doesn't allow GetChanges in an initial sync; sending other options 2038 // appears to cause the server to delay its response in some cases, and this delay 2039 // can be long enough to result in an IOException and total failure to sync. 2040 // Therefore, we don't send any options with the initial sync. 2041 s.tag(Tags.SYNC_DELETES_AS_MOVES); 2042 s.tag(Tags.SYNC_GET_CHANGES); 2043 s.data(Tags.SYNC_WINDOW_SIZE, 2044 className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE); 2045 // Handle options 2046 s.start(Tags.SYNC_OPTIONS); 2047 // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts 2048 if (className.equals("Email")) { 2049 s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter()); 2050 } else if (className.equals("Calendar")) { 2051 // TODO Force two weeks for calendar until we can set this! 2052 s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_2_WEEKS); 2053 } 2054 // Set the truncation amount for all classes 2055 if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 2056 s.start(Tags.BASE_BODY_PREFERENCE) 2057 // HTML for email; plain text for everything else 2058 .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML 2059 : Eas.BODY_PREFERENCE_TEXT)) 2060 .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE) 2061 .end(); 2062 } else { 2063 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 2064 } 2065 s.end(); 2066 } else { 2067 // Use enormous timeout for initial sync, which empirically can take a while longer 2068 timeout = 120*SECONDS; 2069 } 2070 // Send our changes up to the server 2071 target.sendLocalChanges(s); 2072 2073 s.end().end().end().done(); 2074 HttpResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()), 2075 timeout); 2076 int code = resp.getStatusLine().getStatusCode(); 2077 if (code == HttpStatus.SC_OK) { 2078 // In EAS 12.1, we can get "empty" sync responses, which indicate that there are 2079 // no changes in the mailbox; handle that case here 2080 Header header = resp.getFirstHeader("content-length"); 2081 if (header != null && header.getValue().equals("0")) { 2082 // If this happens, exit cleanly, and change the interval from push to ping 2083 // if necessary 2084 mExitStatus = EXIT_DONE; 2085 userLog("Empty sync response; finishing"); 2086 if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) { 2087 userLog("Changing mailbox from push to ping"); 2088 ContentValues cv = new ContentValues(); 2089 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING); 2090 mContentResolver.update( 2091 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId), cv, 2092 null, null); 2093 } 2094 return; 2095 } 2096 InputStream is = resp.getEntity().getContent(); 2097 if (is != null) { 2098 moreAvailable = target.parse(is); 2099 if (target.isLooping()) { 2100 loopingCount++; 2101 userLog("** Looping: " + loopingCount); 2102 // After the maximum number of loops, we'll set moreAvailable to false and 2103 // allow the sync loop to terminate 2104 if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) { 2105 userLog("** Looping force stopped"); 2106 moreAvailable = false; 2107 } 2108 } else { 2109 loopingCount = 0; 2110 } 2111 target.cleanup(); 2112 } else { 2113 userLog("Empty input stream in sync command response"); 2114 } 2115 } else { 2116 userLog("Sync response error: ", code); 2117 if (isProvisionError(code)) { 2118 mExitStatus = EXIT_SECURITY_FAILURE; 2119 } else if (isAuthError(code)) { 2120 mExitStatus = EXIT_LOGIN_FAILURE; 2121 } else { 2122 mExitStatus = EXIT_IO_ERROR; 2123 } 2124 return; 2125 } 2126 } 2127 mExitStatus = EXIT_DONE; 2128 } 2129 2130 protected boolean setupService() { 2131 // Make sure account and mailbox are always the latest from the database 2132 mAccount = Account.restoreAccountWithId(mContext, mAccount.mId); 2133 if (mAccount == null) return false; 2134 mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId); 2135 if (mMailbox == null) return false; 2136 mThread = Thread.currentThread(); 2137 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 2138 TAG = mThread.getName(); 2139 2140 HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv); 2141 if (ha == null) return false; 2142 mHostAddress = ha.mAddress; 2143 mUserName = ha.mLogin; 2144 mPassword = ha.mPassword; 2145 2146 // Set up our protocol version from the Account 2147 mProtocolVersion = mAccount.mProtocolVersion; 2148 // If it hasn't been set up, start with default version 2149 if (mProtocolVersion == null) { 2150 mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION; 2151 } 2152 mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion); 2153 return true; 2154 } 2155 2156 /* (non-Javadoc) 2157 * @see java.lang.Runnable#run() 2158 */ 2159 public void run() { 2160 if (!setupService()) return; 2161 2162 try { 2163 SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0); 2164 } catch (RemoteException e1) { 2165 // Don't care if this fails 2166 } 2167 2168 // Whether or not we're the account mailbox 2169 try { 2170 mDeviceId = SyncManager.getDeviceId(); 2171 if ((mMailbox == null) || (mAccount == null)) { 2172 return; 2173 } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 2174 runAccountMailbox(); 2175 } else { 2176 AbstractSyncAdapter target; 2177 if (mMailbox.mType == Mailbox.TYPE_CONTACTS) { 2178 target = new ContactsSyncAdapter(mMailbox, this); 2179 } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) { 2180 target = new CalendarSyncAdapter(mMailbox, this); 2181 } else { 2182 target = new EmailSyncAdapter(mMailbox, this); 2183 } 2184 // We loop here because someone might have put a request in while we were syncing 2185 // and we've missed that opportunity... 2186 do { 2187 if (mRequestTime != 0) { 2188 userLog("Looping for user request..."); 2189 mRequestTime = 0; 2190 } 2191 sync(target); 2192 } while (mRequestTime != 0); 2193 } 2194 } catch (EasAuthenticationException e) { 2195 userLog("Caught authentication error"); 2196 mExitStatus = EXIT_LOGIN_FAILURE; 2197 } catch (IOException e) { 2198 String message = e.getMessage(); 2199 userLog("Caught IOException: ", (message == null) ? "No message" : message); 2200 mExitStatus = EXIT_IO_ERROR; 2201 } catch (Exception e) { 2202 userLog("Uncaught exception in EasSyncService", e); 2203 } finally { 2204 int status; 2205 2206 if (!mStop) { 2207 userLog("Sync finished"); 2208 SyncManager.done(this); 2209 switch (mExitStatus) { 2210 case EXIT_IO_ERROR: 2211 status = EmailServiceStatus.CONNECTION_ERROR; 2212 break; 2213 case EXIT_DONE: 2214 status = EmailServiceStatus.SUCCESS; 2215 ContentValues cv = new ContentValues(); 2216 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 2217 String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount; 2218 cv.put(Mailbox.SYNC_STATUS, s); 2219 mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, 2220 mMailboxId), cv, null, null); 2221 break; 2222 case EXIT_LOGIN_FAILURE: 2223 status = EmailServiceStatus.LOGIN_FAILED; 2224 break; 2225 case EXIT_SECURITY_FAILURE: 2226 status = EmailServiceStatus.SECURITY_FAILURE; 2227 // Ask for a new folder list. This should wake up the account mailbox; a 2228 // security error in account mailbox should start the provisioning process 2229 SyncManager.reloadFolderList(mContext, mAccount.mId, true); 2230 break; 2231 default: 2232 status = EmailServiceStatus.REMOTE_EXCEPTION; 2233 errorLog("Sync ended due to an exception."); 2234 break; 2235 } 2236 } else { 2237 userLog("Stopped sync finished."); 2238 status = EmailServiceStatus.SUCCESS; 2239 } 2240 2241 try { 2242 SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0); 2243 } catch (RemoteException e1) { 2244 // Don't care if this fails 2245 } 2246 2247 // Make sure SyncManager knows about this 2248 SyncManager.kick("sync finished"); 2249 } 2250 } 2251} 2252