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